markuplint 5.3.0 → 5.4.0

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
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
+ # [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **markuplint:** skip the config search when it is disabled ([e2e8da9](https://github.com/markuplint/markuplint/commit/e2e8da9a81fb113b726fc30877b16cfab4d53736)), closes [#4409](https://github.com/markuplint/markuplint/issues/4409)
11
+ * **markuplint:** watch a config file the search found broken ([1a51145](https://github.com/markuplint/markuplint/commit/1a51145b3eb34c18c0475205466b608787352b66)), closes [#4413](https://github.com/markuplint/markuplint/issues/4413)
12
+
13
+
6
14
  # [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
7
15
 
8
16
  ### Bug Fixes
@@ -1,4 +1,4 @@
1
- import { ConfigProvider, createPretenderResolver, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
1
+ import { ConfigProvider, configLoadErrorFiles, createPretenderResolver, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
2
2
  import path from 'node:path';
3
3
  import { applyRuleAliasesToConfig, mergeConfig } from '@markuplint/ml-config';
4
4
  import { CONFIG_ERROR_RULE_ID, MLCore, convertRuleset } from '@markuplint/ml-core';
@@ -620,7 +620,7 @@ export class MLEngine extends Emitter {
620
620
  // in the middle of this one's `set()`/`search()` calls, which would
621
621
  // wipe the keys just registered below before `resolve()` gets to use
622
622
  // them. See #4015.
623
- const resolvedConfigSet = await this.#configProvider.runExclusive(async () => {
623
+ const resolvedConfigSet = await this.#runResolution(async () => {
624
624
  if (!cache) {
625
625
  // Must run before any `set()` call below — `ConfigProvider#resolve()`
626
626
  // no longer clears its own store on `cache: false`, so invalidating
@@ -632,17 +632,24 @@ export class MLEngine extends Emitter {
632
632
  this.#configProvider.set(mergeConfig(this.#options.defaultConfig), undefined, this.#options.defaultConfig);
633
633
  configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
634
634
  this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
635
- const targetConfig = await this.#configProvider.search(this.#file);
636
- this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
637
- const configFilePathsFromTarget = this.#options?.noSearchConfig || this.#options?.configFile
638
- ? (defaultConfigKey ?? null)
639
- : (targetConfig ?? defaultConfigKey);
635
+ // Skipped, not run and discarded: the search reads and parses every
636
+ // config file it reaches, so a malformed one would still surface as a
637
+ // `config-error` in a run that asked not to search. See #4409.
638
+ let targetConfig = null;
639
+ if (this.#options?.noSearchConfig || this.#options?.configFile) {
640
+ this.emit('log', 'targetConfig', 'skipped (search is disabled)');
641
+ }
642
+ else {
643
+ targetConfig = await this.#configProvider.search(this.#file);
644
+ this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
645
+ }
646
+ const configFilePathsFromTarget = targetConfig ?? defaultConfigKey ?? null;
640
647
  configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
641
648
  this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
642
649
  const configKey = this.#options?.config &&
643
650
  this.#configProvider.set(mergeConfig(this.#options.config), undefined, this.#options.config);
644
651
  configLog('option.config: %s', configKey ?? 'N/A');
645
- this.emit('log', 'option.config', configFilePathsFromTarget ?? 'N/A');
652
+ this.emit('log', 'option.config', configKey ?? 'N/A');
646
653
  let defaultRecommended = null;
647
654
  if (!defaultConfigKey && !configFilePathsFromTarget && !configKey && !this.#options?.configFile) {
648
655
  // No configured
@@ -675,6 +682,38 @@ export class MLEngine extends Emitter {
675
682
  await this.#syncWatch();
676
683
  return configSet;
677
684
  }
685
+ /**
686
+ * Runs `resolve` exclusively on the provider (see {@link resolveConfig}).
687
+ * When it throws a `ConfigLoadError` (a config file that the search found
688
+ * cannot be loaded, or a preset that is not found), the files whose change
689
+ * can fix it ({@link configLoadErrorFiles}: the file it names, and a
690
+ * `$import` target that does not exist yet) are added to the watched files
691
+ * before the error goes on: there is no config set to take
692
+ * {@link ConfigSet.files} from, and the files of the previous resolution
693
+ * alone would miss the fix of a file that was broken from the start. They
694
+ * stay watched, as the error names only the first thing that failed, not
695
+ * every file the config depends on. A directory that the search cannot
696
+ * examine is not among them, as the shared watcher takes file paths (see
697
+ * its module JSDoc in `shared-watcher.ts`).
698
+ *
699
+ * @param resolve - The resolution, which runs inside `runExclusive`
700
+ * @returns The config set that `resolve` returns
701
+ */
702
+ async #runResolution(resolve) {
703
+ try {
704
+ return await this.#configProvider.runExclusive(resolve);
705
+ }
706
+ catch (error) {
707
+ if (isFatalError(error)) {
708
+ throw error;
709
+ }
710
+ if (error instanceof ConfigLoadError) {
711
+ this.#configFiles = new Set([...this.#configFiles, ...configLoadErrorFiles(error)]);
712
+ await this.#syncWatch();
713
+ }
714
+ throw error;
715
+ }
716
+ }
678
717
  async #resolveParser(
679
718
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
680
719
  configSet) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markuplint",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "An HTML linter for all markup developers",
5
5
  "author": "Yusuke Hirao",
6
6
  "license": "MIT",
@@ -35,16 +35,16 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@markuplint/cli-utils": "5.3.0",
38
- "@markuplint/file-resolver": "5.3.0",
39
- "@markuplint/html-parser": "5.3.0",
40
- "@markuplint/html-spec": "5.3.0",
38
+ "@markuplint/file-resolver": "5.4.0",
39
+ "@markuplint/html-parser": "5.4.0",
40
+ "@markuplint/html-spec": "5.4.0",
41
41
  "@markuplint/i18n": "5.3.0",
42
42
  "@markuplint/ml-ast": "5.3.0",
43
- "@markuplint/ml-config": "5.3.0",
44
- "@markuplint/ml-core": "5.3.0",
45
- "@markuplint/ml-spec": "5.3.0",
46
- "@markuplint/rules": "5.3.0",
47
- "@markuplint/shared": "5.3.0",
43
+ "@markuplint/ml-config": "5.4.0",
44
+ "@markuplint/ml-core": "5.4.0",
45
+ "@markuplint/ml-spec": "5.4.0",
46
+ "@markuplint/rules": "5.4.0",
47
+ "@markuplint/shared": "5.4.0",
48
48
  "@types/debug": "^4.1.13",
49
49
  "chokidar": "^5.0.0",
50
50
  "debug": "^4.4.3",
@@ -54,5 +54,5 @@
54
54
  "strip-ansi": "^7.2.0",
55
55
  "type-fest": "^5.10.0"
56
56
  },
57
- "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
57
+ "gitHead": "647f6fb5d5743f060678599c97e9c84b58970747"
58
58
  }