markuplint 4.18.4 → 4.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -170,7 +170,7 @@ flowchart TD
170
170
  2. options.configFile -- 明示的な設定ファイルパス(--config フラグ)
171
171
  3. ConfigProvider.search()-- ファイル位置からの自動探索(--no-search-config でなければ)
172
172
  4. options.defaultConfig -- フォールバック設定
173
- 5. markuplint:recommended -- 設定が一切見つからない場合のデフォルト
173
+ 5. markuplint:recommended -- 設定が一切見つからない場合のデフォルト(`configFile` も指定されていない場合)
174
174
  ```
175
175
 
176
176
  これらは `ConfigProvider.resolve()` で結合され、`@markuplint/ml-config` の `mergeConfig()` を使用して全レイヤーをマージします。
package/ARCHITECTURE.md CHANGED
@@ -170,7 +170,7 @@ The `resolveConfig()` method resolves configuration from multiple sources with t
170
170
  2. options.configFile -- Explicit config file path (--config flag)
171
171
  3. ConfigProvider.search()-- Auto-discovery from file location (unless --no-search-config)
172
172
  4. options.defaultConfig -- Fallback config
173
- 5. markuplint:recommended -- Default when no config is found at all
173
+ 5. markuplint:recommended -- Default when no config is found at all (and no `configFile` is given)
174
174
  ```
175
175
 
176
176
  These are combined via `ConfigProvider.resolve()` which merges all layers using `@markuplint/ml-config`'s `mergeConfig()`.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.18.5](https://github.com/markuplint/markuplint/compare/v4.18.4...v4.18.5) (2026-10-27)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **markuplint:** do not search for a configuration file when noSearchConfig is set ([1163e16](https://github.com/markuplint/markuplint/commit/1163e16afc265b8f2fc05345f6ceed6d3232a105)), closes [#4409](https://github.com/markuplint/markuplint/issues/4409)
11
+ - **markuplint:** keep inline configurations when the configuration is resolved again ([b0d5d0b](https://github.com/markuplint/markuplint/commit/b0d5d0bb449c695e563678cd5c3719c109edad7a)), closes [#4415](https://github.com/markuplint/markuplint/issues/4415)
12
+
6
13
  ## [4.18.4](https://github.com/markuplint/markuplint/compare/v4.18.3...v4.18.4) (2026-10-22)
7
14
 
8
15
  **Note:** Version bump only for package markuplint
package/SKILL.md CHANGED
@@ -88,7 +88,7 @@ Change how MLEngine resolves and merges configuration. Follow recipe #3 in `docs
88
88
 
89
89
  1. Read `src/api/ml-engine.ts` and locate `resolveConfig()`
90
90
  2. Current priority: options.config -> configFile -> search -> defaultConfig -> recommended
91
- 3. Understand `ConfigProvider` methods: `search()`, `set()`, `resolve()` (from `@markuplint/file-resolver`)
91
+ 3. Understand `ConfigProvider` methods: `search()`, `set()`, `resolve()`, `invalidate()`, `runExclusive()` (from `@markuplint/file-resolver`); `resolve()` with `cache` `false` clears the store, including what `set()` has just registered
92
92
 
93
93
  ### Step 2: Modify the logic
94
94
 
@@ -119,7 +119,9 @@ expect(violations).toStrictEqual([
119
119
  3. `@markuplint/file-resolver` の `ConfigProvider` を理解:
120
120
  - `set(config)` で設定を登録しキーを返す
121
121
  - `search(file)` でターゲットに最も近い設定ファイルを検索
122
- - `resolve(file, keys, cache)` で全設定レイヤーをマージ
122
+ - `resolve(file, keys, cache)` で全設定レイヤーをマージ。`cache` が `false` のときは最初にストアを消すため、直前に `set()` で登録した設定も消える
123
+ - `invalidate()` でストアとキャッシュを消す。インライン設定を残したまま読み直すには、`set()` の前に呼び、`cache` を `true` にして `resolve()` する(`resolveConfig(false)` がそうしている)
124
+ - `runExclusive(fn)` で `invalidate()` → `set()` / `search()` → `resolve()` の一連を、重なる呼び出しに割り込まれずに実行する
123
125
  4. イベント発行(`this.emit('config', ...)`)を保持しつつ `resolveConfig()` を変更
124
126
  5. 新しい API オプションを追加する場合、`src/api/types.ts` の `APIOptions` を更新
125
127
  6. `src/api/ml-engine.spec.ts` にテストを追加
@@ -119,7 +119,9 @@ expect(violations).toStrictEqual([
119
119
  3. Understand `ConfigProvider` from `@markuplint/file-resolver`:
120
120
  - `set(config)` registers a config and returns a key
121
121
  - `search(file)` finds the nearest config file for a target
122
- - `resolve(file, keys, cache)` merges all config layers
122
+ - `resolve(file, keys, cache)` merges all config layers; with `cache` `false` it first clears the store, which also discards what `set()` has just registered
123
+ - `invalidate()` clears the store and the caches; call it before `set()`, then `resolve()` with `cache` `true`, for a re-read that keeps the inline configs (`resolveConfig(false)` does)
124
+ - `runExclusive(fn)` runs a whole `invalidate()` → `set()` / `search()` → `resolve()` sequence without an overlapping call interleaving
123
125
  4. Make changes to `resolveConfig()`, preserving the event emissions (`this.emit('config', ...)`)
124
126
  5. If adding new API options, update `APIOptions` in `src/api/types.ts`
125
127
  6. Add tests in `src/api/ml-engine.spec.ts`
@@ -74,6 +74,21 @@ export declare class MLEngine extends Emitter<MLEngineEventMap> {
74
74
  private i18n;
75
75
  private onChange;
76
76
  private provide;
77
+ /**
78
+ * Resolves the configuration for the target file: the configuration found by searching
79
+ * upward from the file (else `defaultConfig`), then `configFile`, then `config`.
80
+ * `markuplint:recommended` is used only when none of them is given.
81
+ *
82
+ * With `noSearchConfig` the search is not run at all, because it throws for a
83
+ * configuration file that cannot be loaded anywhere up to the file system root.
84
+ * `defaultConfig` still applies. `configFile` does not stop the search.
85
+ *
86
+ * `cache` set to `false` reads every configuration file again, and the inline configurations
87
+ * (`defaultConfig`, `config` and the `markuplint:recommended` fallback) are kept (#4415).
88
+ *
89
+ * @param cache - Whether to reuse cached configuration; `false` reads the configuration files again
90
+ * @returns The merged configuration with the plugins, files and errors collected on the way
91
+ */
77
92
  resolveConfig(cache: boolean): Promise<ConfigSet>;
78
93
  private resolveParser;
79
94
  private resolvePretenders;
@@ -294,31 +294,62 @@ export class MLEngine extends Emitter {
294
294
  configErrors: configSet.errs,
295
295
  };
296
296
  }
297
+ /**
298
+ * Resolves the configuration for the target file: the configuration found by searching
299
+ * upward from the file (else `defaultConfig`), then `configFile`, then `config`.
300
+ * `markuplint:recommended` is used only when none of them is given.
301
+ *
302
+ * With `noSearchConfig` the search is not run at all, because it throws for a
303
+ * configuration file that cannot be loaded anywhere up to the file system root.
304
+ * `defaultConfig` still applies. `configFile` does not stop the search.
305
+ *
306
+ * `cache` set to `false` reads every configuration file again, and the inline configurations
307
+ * (`defaultConfig`, `config` and the `markuplint:recommended` fallback) are kept (#4415).
308
+ *
309
+ * @param cache - Whether to reuse cached configuration; `false` reads the configuration files again
310
+ * @returns The merged configuration with the plugins, files and errors collected on the way
311
+ */
297
312
  async resolveConfig(cache) {
298
313
  this.emit('log', 'resolveConfig', JSON.stringify(__classPrivateFieldGet(this, _MLEngine_configProvider, "f"), null, 2));
299
314
  configLog('configProvider: %s', __classPrivateFieldGet(this, _MLEngine_configProvider, "f"));
300
- const defaultConfigKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig));
301
- configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
302
- this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
303
- const targetConfig = await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").search(__classPrivateFieldGet(this, _MLEngine_file, "f"));
304
- this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
305
- const configFilePathsFromTarget = __classPrivateFieldGet(this, _MLEngine_options, "f")?.noSearchConfig
306
- ? (defaultConfigKey ?? null)
307
- : (targetConfig ?? defaultConfigKey);
308
- configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
309
- this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
310
- const configKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.config && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f").config));
311
- configLog('option.config: %s', configKey ?? 'N/A');
312
- this.emit('log', 'option.config', configFilePathsFromTarget ?? 'N/A');
313
- let defaultRecommended = null;
314
- if (!defaultConfigKey && !configFilePathsFromTarget && !configKey) {
315
- // No configured
316
- // Default: set recommended
317
- defaultRecommended = __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set({ extends: ['markuplint:recommended'] });
318
- }
319
- configLog('defaultRecommended: %s', defaultRecommended ?? 'N/A');
320
- this.emit('log', 'defaultRecommended', defaultRecommended ?? 'N/A');
321
- const configSet = await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").resolve(__classPrivateFieldGet(this, _MLEngine_file, "f"), [configFilePathsFromTarget, __classPrivateFieldGet(this, _MLEngine_options, "f")?.configFile, configKey, defaultRecommended], cache);
315
+ // The whole invalidate → set → search → resolve sequence runs as one exclusive unit: an
316
+ // overlapping call on this engine's provider (a watch-triggered `provide(false)` while
317
+ // `exec()` or `--show-config` resolves) must not clear the configurations registered below
318
+ // before `resolve()` has used them (#4415).
319
+ const configSet = await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").runExclusive(async () => {
320
+ if (!cache) {
321
+ // Before any `set()` below. `resolve()` with `cache` set to `false` would clear the store
322
+ // itself, after the inline configurations were registered, and then fail to load their
323
+ // keys as file paths (#4415). Everything is cleared here instead, and `resolve()` is
324
+ // called with `cache` as `true`: what was cleared is read again.
325
+ __classPrivateFieldGet(this, _MLEngine_configProvider, "f").invalidate();
326
+ }
327
+ const defaultConfigKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f")?.defaultConfig));
328
+ configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
329
+ this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
330
+ // Not searching at all (#4409), rather than discarding the result: the search throws for a
331
+ // configuration file that cannot be loaded anywhere up to the file system root.
332
+ const noSearch = !!__classPrivateFieldGet(this, _MLEngine_options, "f")?.noSearchConfig;
333
+ const targetConfig = noSearch ? null : await __classPrivateFieldGet(this, _MLEngine_configProvider, "f").search(__classPrivateFieldGet(this, _MLEngine_file, "f"));
334
+ this.emit('log', 'targetConfig', noSearch ? 'skipped (noSearchConfig)' : (targetConfig ?? 'N/A'));
335
+ // The configuration that stands in for the target's own: the search result, else `defaultConfig`.
336
+ const configFilePathsFromTarget = targetConfig ?? defaultConfigKey ?? null;
337
+ configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
338
+ this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
339
+ const configKey = __classPrivateFieldGet(this, _MLEngine_options, "f")?.config && __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set(mergeConfig(__classPrivateFieldGet(this, _MLEngine_options, "f").config));
340
+ configLog('option.config: %s', configKey ?? 'N/A');
341
+ this.emit('log', 'option.config', configKey ?? 'N/A');
342
+ let defaultRecommended = null;
343
+ if (!defaultConfigKey && !configFilePathsFromTarget && !configKey && !__classPrivateFieldGet(this, _MLEngine_options, "f")?.configFile) {
344
+ // No configured
345
+ // Default: set recommended
346
+ // Not with `configFile`: the preset is merged after it and would override its rules (#4415).
347
+ defaultRecommended = __classPrivateFieldGet(this, _MLEngine_configProvider, "f").set({ extends: ['markuplint:recommended'] });
348
+ }
349
+ configLog('defaultRecommended: %s', defaultRecommended ?? 'N/A');
350
+ this.emit('log', 'defaultRecommended', defaultRecommended ?? 'N/A');
351
+ return __classPrivateFieldGet(this, _MLEngine_configProvider, "f").resolve(__classPrivateFieldGet(this, _MLEngine_file, "f"), [configFilePathsFromTarget, __classPrivateFieldGet(this, _MLEngine_options, "f")?.configFile, configKey, defaultRecommended], true);
352
+ });
322
353
  this.emit('config', __classPrivateFieldGet(this, _MLEngine_file, "f").path, configSet);
323
354
  if (__classPrivateFieldGet(this, _MLEngine_options, "f")?.watch) {
324
355
  // It doesn't watch the main HTML file because it may is watched and managed by a language server or text editor or more.
@@ -6,9 +6,18 @@ import type { AnyMLRule, MLSchema, Ruleset } from '@markuplint/ml-core';
6
6
  * Options for the markuplint API, controlling configuration, locale, rules, and behavior.
7
7
  */
8
8
  export type APIOptions = {
9
+ /**
10
+ * The path of a configuration file. When it is given, `markuplint:recommended` is not added
11
+ * after it as the fallback, so the file's own rules are not overridden (#4415).
12
+ */
9
13
  readonly configFile?: string;
10
14
  readonly config?: Config;
11
15
  readonly defaultConfig?: Config;
16
+ /**
17
+ * Do not search for a configuration file from the target file. No configuration file is
18
+ * read for the search, so a malformed one cannot affect the run. `defaultConfig`, `config`
19
+ * and `configFile` still apply; `configFile` alone does not disable the search.
20
+ */
12
21
  readonly noSearchConfig?: boolean;
13
22
  readonly locale?: string;
14
23
  readonly fix?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markuplint",
3
- "version": "4.18.4",
3
+ "version": "4.18.5",
4
4
  "description": "An HTML linter for all markup developers",
5
5
  "author": "Yusuke Hirao",
6
6
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@markuplint/cli-utils": "4.18.0",
27
- "@markuplint/file-resolver": "4.18.4",
27
+ "@markuplint/file-resolver": "4.18.5",
28
28
  "@markuplint/html-parser": "4.18.4",
29
29
  "@markuplint/html-spec": "4.18.4",
30
30
  "@markuplint/i18n": "4.18.0",
@@ -43,5 +43,5 @@
43
43
  "strip-ansi": "7.2.0",
44
44
  "type-fest": "5.10.0"
45
45
  },
46
- "gitHead": "b5fbb4572f7313e3c80735da1677d33cb82e6e2f"
46
+ "gitHead": "e7307cd5bdb771ac91e723c4c8869499190f2465"
47
47
  }