markuplint 5.0.0-rc.4 → 5.0.0-rc.6

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +3 -2
  3. package/lib/api/lint.d.ts +5 -0
  4. package/lib/api/lint.js +40 -3
  5. package/lib/api/ml-engine.d.ts +60 -2
  6. package/lib/api/ml-engine.js +103 -30
  7. package/lib/cli/bootstrap.d.ts +5 -15
  8. package/lib/cli/bootstrap.js +7 -13
  9. package/lib/cli/command.d.ts +0 -12
  10. package/lib/cli/command.js +74 -24
  11. package/lib/cli/dry-run-output.d.ts +0 -8
  12. package/lib/cli/dry-run-output.js +0 -8
  13. package/lib/cli/index.d.ts +4 -3
  14. package/lib/cli/index.js +4 -3
  15. package/lib/cli/init/create-config.d.ts +0 -16
  16. package/lib/cli/init/create-config.js +0 -20
  17. package/lib/cli/init/get-default-rules.d.ts +0 -9
  18. package/lib/cli/init/get-default-rules.js +0 -9
  19. package/lib/cli/init/index.d.ts +0 -14
  20. package/lib/cli/init/index.js +18 -20
  21. package/lib/cli/init/select-modules.d.ts +0 -10
  22. package/lib/cli/init/select-modules.js +0 -10
  23. package/lib/cli/init/types.d.ts +1 -20
  24. package/lib/cli/output.d.ts +0 -21
  25. package/lib/cli/output.js +0 -21
  26. package/lib/cli/search/index.d.ts +0 -17
  27. package/lib/cli/search/index.js +0 -17
  28. package/lib/debug.d.ts +0 -9
  29. package/lib/debug.js +0 -9
  30. package/lib/dedupe-config-violations.d.ts +29 -0
  31. package/lib/dedupe-config-violations.js +41 -0
  32. package/lib/get-json-module.d.ts +0 -10
  33. package/lib/get-json-module.js +0 -10
  34. package/lib/global-settings.d.ts +0 -15
  35. package/lib/global-settings.js +0 -12
  36. package/lib/reporter/github-reporter.d.ts +0 -9
  37. package/lib/reporter/github-reporter.js +0 -9
  38. package/lib/reporter/index.d.ts +0 -9
  39. package/lib/reporter/index.js +0 -9
  40. package/lib/reporter/simple-reporter.d.ts +0 -11
  41. package/lib/reporter/simple-reporter.js +0 -11
  42. package/lib/reporter/standard-reporter.d.ts +0 -12
  43. package/lib/reporter/standard-reporter.js +0 -12
  44. package/lib/suppressions/apply-suppressions.d.ts +9 -0
  45. package/lib/suppressions/apply-suppressions.js +9 -23
  46. package/lib/suppressions/compute-scope.d.ts +17 -16
  47. package/lib/suppressions/compute-scope.js +17 -24
  48. package/lib/suppressions/downgrade-severity.js +0 -3
  49. package/lib/suppressions/index.d.ts +8 -1
  50. package/lib/suppressions/index.js +8 -1
  51. package/lib/testing-tool/index.js +3 -0
  52. package/package.json +14 -14
  53. package/ARCHITECTURE.ja.md +0 -419
  54. package/ARCHITECTURE.md +0 -419
  55. package/SKILL.md +0 -110
  56. package/docs/maintenance.ja.md +0 -207
  57. package/docs/maintenance.md +0 -207
package/CHANGELOG.md CHANGED
@@ -3,6 +3,89 @@
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.0.0-rc.6](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.5...v5.0.0-rc.6) (2026-08-30)
7
+
8
+ ### Bug Fixes
9
+
10
+ - add no-aria-hidden-on-hidden-until-found rule (closes [#3999](https://github.com/markuplint/markuplint/issues/3999)) ([#4017](https://github.com/markuplint/markuplint/issues/4017)) ([3dfb300](https://github.com/markuplint/markuplint/commit/3dfb3001dc66452918d5cebbb071244b68779b30))
11
+ - resolveConfig(false) crashes with inline config (not a file path) ([#4018](https://github.com/markuplint/markuplint/issues/4018)) ([7e38b64](https://github.com/markuplint/markuplint/commit/7e38b64caa8cca69009ee765e4aada37fc48c559)), closes [#4015](https://github.com/markuplint/markuplint/issues/4015) [#4015](https://github.com/markuplint/markuplint/issues/4015)
12
+
13
+ ### Features
14
+
15
+ - split rule-deprecation notices out of config-error ([#4013](https://github.com/markuplint/markuplint/issues/4013)) ([812e6f3](https://github.com/markuplint/markuplint/commit/812e6f356839af8f257cfd91e6b16cfdfdd7cf33))
16
+
17
+ ### Performance Improvements
18
+
19
+ - share ConfigProvider across a run's files, fix latent overrides caching bug ([#4016](https://github.com/markuplint/markuplint/issues/4016)) ([fcc1875](https://github.com/markuplint/markuplint/commit/fcc1875b1a984a5ef1bb36aa04e7b3522fefc58e)), closes [#3997](https://github.com/markuplint/markuplint/issues/3997) [#3997](https://github.com/markuplint/markuplint/issues/3997)
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ - `ConfigProvider#resolve(targetFile, names, false)` no longer
24
+ clears the provider's store/cache/plugin-resolution caches by itself. Callers
25
+ that relied on `cache: false` alone to force a fresh re-read must now call
26
+ the new `ConfigProvider#invalidate()` first.
27
+ - violations for deprecated rule names now have
28
+ `ruleId: 'rule-deprecation'` instead of `ruleId: 'config-error'`. Any
29
+ consumer filtering `MLCore.verify()` output (or the markuplint CLI/API) by
30
+ `ruleId === 'config-error'` to catch deprecation messages must also check
31
+ for `rule-deprecation`.
32
+
33
+ - feat(markuplint): add --severity-deprecation CLI flag
34
+
35
+ Wires the new severity.deprecation config option (@markuplint/ml-config)
36
+ and the rule-deprecation ruleId (@markuplint/ml-core) through the CLI:
37
+
38
+ - --severity-deprecation flag, mirroring --severity-parse-error
39
+ - --show-config details now also surfaces ruleDeprecations
40
+ - per-run dedupe and failed-file counting generalized to cover both
41
+ config-level ruleIds (config-error and rule-deprecation), not just
42
+ config-error
43
+
44
+ * docs(website): document severity.deprecation (EN + JA)
45
+
46
+ # [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
47
+
48
+ ### Bug Fixes
49
+
50
+ - **markuplint:** report post-fix violations for exit code and suppressions ([03855d6](https://github.com/markuplint/markuplint/commit/03855d6641ae736b65994fa6123e91779d2eb41e)), closes [#3890](https://github.com/markuplint/markuplint/issues/3890)
51
+ - **markuplint:** stop repeating config-error messages once per file ([#4007](https://github.com/markuplint/markuplint/issues/4007)) ([466193d](https://github.com/markuplint/markuplint/commit/466193d286628f69e0d99fc8b2f66028523025aa)), closes [#4006](https://github.com/markuplint/markuplint/issues/4006)
52
+ - **pretenders:** resolve same-named components via imports, not scan order ([#3957](https://github.com/markuplint/markuplint/issues/3957)) ([d46a514](https://github.com/markuplint/markuplint/commit/d46a5148c4d7afb156962f4ed795f40a9324e6c5)), closes [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3951](https://github.com/markuplint/markuplint/issues/3951)
53
+ - **rules:** surface disallowed-element reason via reasonOnly (close [#3815](https://github.com/markuplint/markuplint/issues/3815)) ([#3986](https://github.com/markuplint/markuplint/issues/3986)) ([0142cec](https://github.com/markuplint/markuplint/commit/0142cec667f70fee086f2a6e06d7a26e66bda380))
54
+ - **types): strict charset=utf-8; feat(rules:** usemap-references-map ([#3969](https://github.com/markuplint/markuplint/issues/3969)) ([c63070e](https://github.com/markuplint/markuplint/commit/c63070e29ccb283da7468b2fc67db372ebfcf42a)), closes [#3945](https://github.com/markuplint/markuplint/issues/3945) [#3966](https://github.com/markuplint/markuplint/issues/3966) [#3966](https://github.com/markuplint/markuplint/issues/3966) [#3928](https://github.com/markuplint/markuplint/issues/3928)
55
+
56
+ ### Code Refactoring
57
+
58
+ - **rules:** redesign v5 rule system — naming, splits, specConformance ([#3989](https://github.com/markuplint/markuplint/issues/3989)) ([e925565](https://github.com/markuplint/markuplint/commit/e925565ce537848d7d1573369723cbce724a841b)), closes [#4](https://github.com/markuplint/markuplint/issues/4) [#aside-conditional-role-mapping-aria-13](https://github.com/markuplint/markuplint/issues/aside-conditional-role-mapping-aria-13)
59
+
60
+ - feat(markuplint)!: stop forcing severity.parseError default in CLI ([79ff00b](https://github.com/markuplint/markuplint/commit/79ff00b5d068802f4d7e0d8d30ee63e55b8bc7f1)), closes [#3844](https://github.com/markuplint/markuplint/issues/3844)
61
+
62
+ ### Features
63
+
64
+ - add `pretenders.auto` for on-demand import-graph resolution ([#3962](https://github.com/markuplint/markuplint/issues/3962)) ([5870671](https://github.com/markuplint/markuplint/commit/58706711a20c12cff080d49359f3f6443345eca3)), closes [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3959](https://github.com/markuplint/markuplint/issues/3959) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3957](https://github.com/markuplint/markuplint/issues/3957) [#3959](https://github.com/markuplint/markuplint/issues/3959) [#3951](https://github.com/markuplint/markuplint/issues/3951) [#3951](https://github.com/markuplint/markuplint/issues/3951)
65
+ - **config-presets:** forbid <base> after <link> or <script> in <head> ([#3925](https://github.com/markuplint/markuplint/issues/3925)) ([ceb892d](https://github.com/markuplint/markuplint/commit/ceb892d64772d459a6bd9564684218e3afbdec2e))
66
+ - **rules:** add form-attr-references-form rule ([6b541f0](https://github.com/markuplint/markuplint/commit/6b541f032b76c3712c99ea35596b6b0aa79b6137))
67
+ - **rules:** add input-button-non-empty-value rule ([2cc73dd](https://github.com/markuplint/markuplint/commit/2cc73ddd0f874fae9faf005f498d73fa364b682c))
68
+ - **rules:** add input-file-empty-value rule ([228cbd7](https://github.com/markuplint/markuplint/commit/228cbd752956d3df8f525e4f19f9278a44d87160))
69
+ - **rules:** add input-list-references-datalist rule ([#3931](https://github.com/markuplint/markuplint/issues/3931)) ([bf4ef54](https://github.com/markuplint/markuplint/commit/bf4ef54a1b2937ecbe05fbe5121ddfe199781a95))
70
+ - **rules:** add label-for-references-labelable rule ([#3932](https://github.com/markuplint/markuplint/issues/3932)) ([3713e6b](https://github.com/markuplint/markuplint/commit/3713e6b435a76fe03a941a36a5e33c0ab06c9a80)), closes [#3918](https://github.com/markuplint/markuplint/issues/3918)
71
+ - **rules:** add label-no-multiple-controls rule ([5da3f85](https://github.com/markuplint/markuplint/commit/5da3f8523dd6ffd9f44ea73d9012952aad85d821))
72
+ - **rules:** add map-id-name-match rule ([1472daf](https://github.com/markuplint/markuplint/commit/1472daf62470bf56c4cb326ab47dc43ba87a8cb3))
73
+ - **rules:** add no-extra-selected-options rule ([3ea75ac](https://github.com/markuplint/markuplint/commit/3ea75ac0b6850c36d5419924b18c1002dfb864a9))
74
+ - **rules:** add progress-value-bounds rule ([#3926](https://github.com/markuplint/markuplint/issues/3926)) ([1e259ec](https://github.com/markuplint/markuplint/commit/1e259ec9929ceb3c7ac5864ce2807420646e9602))
75
+ - **rules:** add wai-aria-tab-requires-tabpanel rule ([#3955](https://github.com/markuplint/markuplint/issues/3955)) ([eac9abe](https://github.com/markuplint/markuplint/commit/eac9abef20ef304c3da2114849686b9cf0733942))
76
+ - **rules:** surface parse5-silent HTML LS parse errors (close nu-only umbrella [#3943](https://github.com/markuplint/markuplint/issues/3943)) ([#3980](https://github.com/markuplint/markuplint/issues/3980)) ([89951fa](https://github.com/markuplint/markuplint/commit/89951fa274007d56370510cb0cf11aead808ce13))
77
+ - wire script-content into preset, bench, and default-rules ([dd0507a](https://github.com/markuplint/markuplint/commit/dd0507a9f54fcff25dba666a1c8fbc082489bdc8))
78
+
79
+ ### BREAKING CHANGES
80
+
81
+ - **rules:** with no alias coverage.
82
+ - CLI invocations that previously relied on the implicit
83
+ \`severity.parseError: 'error'\` default for _fatal_ parser errors are
84
+ unaffected — fatal errors continue to emit at \`'error'\` regardless. But
85
+ projects that lint malformed HTML and expected non-fatal parse5 events to
86
+ show up by default must now pass \`--severity-parse-error error\` (or set
87
+ \`severity.parseError\` in their config).
88
+
6
89
  # [5.0.0-rc.4](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.3...v5.0.0-rc.4) (2026-04-19)
7
90
 
8
91
  **Note:** Version bump only for package markuplint
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  $ npx markuplint target.html
26
26
  ```
27
27
 
28
- Supported for _Node.js_ `v22.0.0` or later.
28
+ Supported for _Node.js_ `v24.0.0` or later.
29
29
 
30
30
  ## Usage
31
31
 
@@ -87,6 +87,7 @@ Options
87
87
  --verbose Output with detailed information.
88
88
  --include-node-modules Include files in node_modules directory. Default: false.
89
89
  --severity-parse-error Specifies the severity level of parse errors. Supports "error", "warning", and "off". Default: "error".
90
+ --severity-deprecation Specifies the severity level of deprecated rule name notices. Supports "error", "warning", and "off". Default: "warning".
90
91
  --max-count Limit the number of violations shown. Default: 0 (no limit).
91
92
  --max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).
92
93
  --progressive-output Output results immediately after processing each file. Default: false.
@@ -138,7 +139,7 @@ $ markuplint --prune-suppressions "src/**/*.html"
138
139
  ```json
139
140
  {
140
141
  "src/index.html": {
141
- "attr-duplication": { "count": 3, "scope": "#main-nav > ul" }
142
+ "no-duplicate-attr": { "count": 3, "scope": "#main-nav > ul" }
142
143
  }
143
144
  }
144
145
  ```
package/lib/api/lint.d.ts CHANGED
@@ -4,6 +4,11 @@ import type { Target } from '@markuplint/file-resolver';
4
4
  /**
5
5
  * Lints multiple targets (files or inline sources) and returns results for each.
6
6
  *
7
+ * Config-level violations (broken config, deprecated rule names) are deduped
8
+ * per call the same way the CLI dedupes them per run: a message identical
9
+ * across every file sharing one config is kept only in the first file's
10
+ * result, not repeated in every file's `violations` array. See #3997.
11
+ *
7
12
  * @param targetList - An array of file paths/globs or inline source code targets
8
13
  * @param options - API options for configuration, locale, rules, and behavior
9
14
  * @returns An array of lint results, one per processed file
package/lib/api/lint.js CHANGED
@@ -1,8 +1,14 @@
1
- import { resolveFiles } from '@markuplint/file-resolver';
1
+ import { ConfigProvider, resolveFiles } from '@markuplint/file-resolver';
2
+ import { dedupeConfigLevelViolations } from '../dedupe-config-violations.js';
2
3
  import { MLEngine } from './ml-engine.js';
3
4
  /**
4
5
  * Lints multiple targets (files or inline sources) and returns results for each.
5
6
  *
7
+ * Config-level violations (broken config, deprecated rule names) are deduped
8
+ * per call the same way the CLI dedupes them per run: a message identical
9
+ * across every file sharing one config is kept only in the first file's
10
+ * result, not repeated in every file's `violations` array. See #3997.
11
+ *
6
12
  * @param targetList - An array of file paths/globs or inline source code targets
7
13
  * @param options - API options for configuration, locale, rules, and behavior
8
14
  * @returns An array of lint results, one per processed file
@@ -10,13 +16,44 @@ import { MLEngine } from './ml-engine.js';
10
16
  export async function lint(targetList, options) {
11
17
  const res = [];
12
18
  const files = await resolveFiles(targetList);
19
+ // Shared across every file so `MLEngine`'s config cache — keyed by
20
+ // resolved config `names`, not by target file — actually helps: see
21
+ // `ConfigProvider.resolve`'s doc comment and #3997.
22
+ const configProvider = new ConfigProvider();
23
+ const seenConfigMessages = new Set();
13
24
  for (const file of files) {
14
- const engine = new MLEngine(file, options);
25
+ const engine = new MLEngine(file, { ...options, configProvider });
15
26
  const result = await engine.exec();
16
27
  if (!result) {
17
28
  continue;
18
29
  }
19
- res.push(result);
30
+ // Dedupe both views a caller might read: the first-pass `violations`,
31
+ // and — when fix mode found fixes — `fixSummary.finalPassViolations`
32
+ // (the post-fix re-verification `command.ts` prefers when reporting
33
+ // fixed results). Both arrays describe the SAME file, so they always
34
+ // carry the same config-level messages — that's not a repeat worth
35
+ // suppressing. Only a *later file* repeating a message already kept
36
+ // (in either array) is. So both arrays are deduped against the same
37
+ // pre-file snapshot of `seenConfigMessages` (not against each other),
38
+ // and only after both are done are their combined discoveries folded
39
+ // back into `seenConfigMessages` for the next file. The snapshot is
40
+ // only taken when there's a second array to protect against — most
41
+ // files (no fix mode, or fix mode with nothing to fix) have none.
42
+ let finalPassViolations = result.fixSummary?.finalPassViolations;
43
+ const seenBeforeThisFile = finalPassViolations ? new Set(seenConfigMessages) : undefined;
44
+ const violations = dedupeConfigLevelViolations(result.violations, seenConfigMessages);
45
+ if (finalPassViolations && seenBeforeThisFile) {
46
+ const seenForFinalPass = new Set(seenBeforeThisFile);
47
+ finalPassViolations = dedupeConfigLevelViolations(finalPassViolations, seenForFinalPass);
48
+ for (const key of seenForFinalPass) {
49
+ seenConfigMessages.add(key);
50
+ }
51
+ }
52
+ res.push({
53
+ ...result,
54
+ violations,
55
+ fixSummary: result.fixSummary && { ...result.fixSummary, finalPassViolations },
56
+ });
20
57
  }
21
58
  return res;
22
59
  }
@@ -1,12 +1,44 @@
1
1
  import type { APIOptions, MLEngineEventMap } from './types.js';
2
2
  import type { MLResultInfo } from '../types.js';
3
3
  import type { ConfigSet, MLFile, Target } from '@markuplint/file-resolver';
4
- import type { PlainData } from '@markuplint/ml-config';
4
+ import type { PlainData, RuleAliasWarning } from '@markuplint/ml-config';
5
5
  import type { Document, RuleConfigValue } from '@markuplint/ml-core';
6
+ import { ConfigProvider } from '@markuplint/file-resolver';
6
7
  import { Emitter } from 'strict-event-emitter';
7
8
  type MLEngineOptions = {
8
9
  readonly debug?: boolean;
9
10
  readonly watch?: boolean;
11
+ /**
12
+ * A pre-built {@link ConfigProvider} to resolve config through, instead of
13
+ * the one this instance would otherwise create for itself.
14
+ *
15
+ * `ConfigProvider` caches by the resolved config's `names` (file paths),
16
+ * not by target file — so a caller looping over many files (the CLI, or
17
+ * the `lint()` API) that constructs one `MLEngine` per file should share
18
+ * a single `ConfigProvider` across that loop; every file whose config
19
+ * resolves to the same `names` then reuses the same cached base config
20
+ * (merge/validate/plugin-resolution) instead of redoing that work once
21
+ * per file. See #3997.
22
+ *
23
+ * Optional and additive: omitted, each `MLEngine` still creates its own
24
+ * provider exactly as before.
25
+ *
26
+ * **Caveat — `watch: true`**: `resolveConfig()`'s cache-busting
27
+ * (`cache: false`, used internally on every watch-triggered re-resolve)
28
+ * calls `ConfigProvider#invalidate()`, which clears the *entire* shared
29
+ * provider, not anything scoped to one engine or file. `resolveConfig()`
30
+ * runs through `ConfigProvider#runExclusive()`, so an overlapping call
31
+ * from another engine can no longer interleave with — and corrupt — this
32
+ * one's in-flight resolve (see #4015); it can only run *before* or
33
+ * *after* it. Sharing one `configProvider` across multiple engines that
34
+ * also have `watch: true` still risks one engine's re-resolve evicting
35
+ * another's already-cached config, forcing an avoidable re-resolve on
36
+ * that engine's next lookup. Neither the CLI (which doesn't support
37
+ * `--watch`) nor `lint()` (which never sets `watch`) create this
38
+ * combination — it only arises if a direct API consumer builds it
39
+ * deliberately.
40
+ */
41
+ readonly configProvider?: ConfigProvider;
10
42
  };
11
43
  /**
12
44
  * Options for creating an {@link MLEngine} from inline source code.
@@ -17,6 +49,16 @@ export type FromCodeOptions = APIOptions & MLEngineOptions & {
17
49
  /** Optional working directory for config resolution */
18
50
  readonly dirname?: string;
19
51
  };
52
+ /**
53
+ * A {@link ConfigSet} widened with deprecated-rule-name notices found while
54
+ * applying {@link applyRuleAliasesToConfig}. Kept structured (not folded
55
+ * into `errs` as generic `Error`s) so `MLCore` can report them under their
56
+ * own `rule-deprecation` ruleId instead of `config-error` — see
57
+ * `packages/@markuplint/ml-core/src/ml-core.ts`'s `verify()`.
58
+ */
59
+ type ResolvedConfigSet = ConfigSet & {
60
+ readonly ruleDeprecations: readonly RuleAliasWarning[];
61
+ };
20
62
  /**
21
63
  * The main markuplint engine that orchestrates file resolution, configuration loading,
22
64
  * parsing, and linting. Supports both single-file and watch-mode operation.
@@ -70,6 +112,22 @@ export declare class MLEngine extends Emitter<MLEngineEventMap> {
70
112
  * @param enable - Whether to enable watch mode
71
113
  */
72
114
  watchMode(enable: boolean): void;
73
- resolveConfig(cache: boolean): Promise<ConfigSet>;
115
+ /**
116
+ * Resolves the configuration set for the target file.
117
+ *
118
+ * Public — unlike the other resolution steps, which are private —
119
+ * because the CLI's `--show-config` needs the computed configuration
120
+ * without running a lint.
121
+ *
122
+ * Precedence contract (highest first): the inline `config` option,
123
+ * then the explicit `configFile` path, then auto-discovered config files
124
+ * (search is skipped when `noSearchConfig` or `configFile` is set),
125
+ * then `defaultConfig`. `markuplint:recommended` applies only when none
126
+ * of these are provided.
127
+ *
128
+ * @param cache - Whether to reuse previously loaded config files
129
+ * @returns The resolved configuration set
130
+ */
131
+ resolveConfig(cache: boolean): Promise<ResolvedConfigSet>;
74
132
  }
75
133
  export {};
@@ -1,6 +1,7 @@
1
- import { ConfigProvider, resolveFiles, resolveParser, resolvePretenders, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
2
- import { mergeConfig } from '@markuplint/ml-config';
1
+ import { ConfigProvider, disambiguatePretendersForFile, invalidatePretenderResolutionCaches, resolveFiles, resolveParser, resolvePretenders, resolveRules, resolveSpecs, } from '@markuplint/file-resolver';
2
+ import { applyRuleAliasesToConfig, mergeConfig } from '@markuplint/ml-config';
3
3
  import { MLCore, convertRuleset } from '@markuplint/ml-core';
4
+ import { ruleAliasTable } from '@markuplint/rules';
4
5
  import { isFatalError } from '@markuplint/shared';
5
6
  import { FSWatcher } from 'chokidar';
6
7
  import { Emitter } from 'strict-event-emitter';
@@ -9,6 +10,15 @@ import { i18n } from '../i18n.js';
9
10
  const log = coreLog.extend('ml-engine');
10
11
  const fileLog = log.extend('file');
11
12
  const configLog = log.extend('config');
13
+ /**
14
+ * The config passed to {@link ConfigProvider.set} when no config was
15
+ * discovered, requested, or provided by the caller. A module-level constant
16
+ * (not rebuilt per call) so its object identity is stable — letting
17
+ * `ConfigProvider.set`'s auto-key cache (keyed by identity) recognize repeat
18
+ * calls across every file in a run sharing one `ConfigProvider`, the same
19
+ * way a stable `options.config`/`defaultConfig` reference does. See #3997.
20
+ */
21
+ const RECOMMENDED_CONFIG = { extends: ['markuplint:recommended'] };
12
22
  /**
13
23
  * The main markuplint engine that orchestrates file resolution, configuration loading,
14
24
  * parsing, and linting. Supports both single-file and watch-mode operation.
@@ -62,7 +72,7 @@ export class MLEngine extends Emitter {
62
72
  }
63
73
  this.#file = file;
64
74
  this.#options = options;
65
- this.#configProvider = new ConfigProvider();
75
+ this.#configProvider = options?.configProvider ?? new ConfigProvider();
66
76
  this.watchMode(!!this.#options?.watch);
67
77
  log('[MLEngine] Initialized: %s', this.#file.path);
68
78
  }
@@ -224,6 +234,7 @@ export class MLEngine extends Emitter {
224
234
  plugins: [],
225
235
  files: new Set(),
226
236
  errs: [error],
237
+ ruleDeprecations: [],
227
238
  };
228
239
  }
229
240
  else {
@@ -256,7 +267,7 @@ export class MLEngine extends Emitter {
256
267
  ...configSet.config.severity,
257
268
  ...this.#options?.severity,
258
269
  };
259
- const pretenders = await this.#resolvePretenders(configSet);
270
+ const pretenders = await this.#resolvePretenders(configSet, cache);
260
271
  fileLog('Resolved pretenders: %O', pretenders);
261
272
  const ruleset = this.#resolveRuleset(configSet);
262
273
  fileLog('Resolved ruleset: %O', ruleset);
@@ -291,33 +302,83 @@ export class MLEngine extends Emitter {
291
302
  locale,
292
303
  ruleCommonSettings,
293
304
  configErrors: configSet.errs,
305
+ ruleDeprecations: configSet.ruleDeprecations,
294
306
  };
295
307
  }
308
+ /**
309
+ * Resolves the configuration set for the target file.
310
+ *
311
+ * Public — unlike the other resolution steps, which are private —
312
+ * because the CLI's `--show-config` needs the computed configuration
313
+ * without running a lint.
314
+ *
315
+ * Precedence contract (highest first): the inline `config` option,
316
+ * then the explicit `configFile` path, then auto-discovered config files
317
+ * (search is skipped when `noSearchConfig` or `configFile` is set),
318
+ * then `defaultConfig`. `markuplint:recommended` applies only when none
319
+ * of these are provided.
320
+ *
321
+ * @param cache - Whether to reuse previously loaded config files
322
+ * @returns The resolved configuration set
323
+ */
296
324
  async resolveConfig(cache) {
297
325
  this.emit('log', 'resolveConfig', JSON.stringify(this.#configProvider, null, 2));
298
326
  configLog('configProvider: %s', this.#configProvider);
299
- const defaultConfigKey = this.#options?.defaultConfig && this.#configProvider.set(mergeConfig(this.#options?.defaultConfig));
300
- configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
301
- this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
302
- const targetConfig = await this.#configProvider.search(this.#file);
303
- this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
304
- const configFilePathsFromTarget = this.#options?.noSearchConfig || this.#options?.configFile
305
- ? (defaultConfigKey ?? null)
306
- : (targetConfig ?? defaultConfigKey);
307
- configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
308
- this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
309
- const configKey = this.#options?.config && this.#configProvider.set(mergeConfig(this.#options.config));
310
- configLog('option.config: %s', configKey ?? 'N/A');
311
- this.emit('log', 'option.config', configFilePathsFromTarget ?? 'N/A');
312
- let defaultRecommended = null;
313
- if (!defaultConfigKey && !configFilePathsFromTarget && !configKey && !this.#options?.configFile) {
314
- // No configured
315
- // Default: set recommended
316
- defaultRecommended = this.#configProvider.set({ extends: ['markuplint:recommended'] });
317
- }
318
- configLog('defaultRecommended: %s', defaultRecommended ?? 'N/A');
319
- this.emit('log', 'defaultRecommended', defaultRecommended ?? 'N/A');
320
- const configSet = await this.#configProvider.resolve(this.#file, [configFilePathsFromTarget, this.#options?.configFile, configKey, defaultRecommended], cache);
327
+ // Runs the whole invalidate → set → search → resolve sequence as one
328
+ // exclusive unit on the provider — an overlapping call on the same
329
+ // (possibly shared, e.g. two watch-triggered re-resolves close
330
+ // together) `ConfigProvider` must not interleave its own `invalidate()`
331
+ // in the middle of this one's `set()`/`search()` calls, which would
332
+ // wipe the keys just registered below before `resolve()` gets to use
333
+ // them. See #4015.
334
+ const resolvedConfigSet = await this.#configProvider.runExclusive(async () => {
335
+ if (!cache) {
336
+ // Must run before any `set()` call below — `ConfigProvider#resolve()`
337
+ // no longer clears its own store on `cache: false`, so invalidating
338
+ // after registering this call's inline config would discard it
339
+ // again immediately. See #4015.
340
+ this.#configProvider.invalidate();
341
+ }
342
+ const defaultConfigKey = this.#options?.defaultConfig &&
343
+ this.#configProvider.set(mergeConfig(this.#options.defaultConfig), undefined, this.#options.defaultConfig);
344
+ configLog('defaultConfigKey: %s', defaultConfigKey ?? 'N/A');
345
+ this.emit('log', 'defaultConfigKey', defaultConfigKey ?? 'N/A');
346
+ const targetConfig = await this.#configProvider.search(this.#file);
347
+ this.emit('log', 'targetConfig', targetConfig ?? 'N/A');
348
+ const configFilePathsFromTarget = this.#options?.noSearchConfig || this.#options?.configFile
349
+ ? (defaultConfigKey ?? null)
350
+ : (targetConfig ?? defaultConfigKey);
351
+ configLog('configFilePathsFromTarget: %s', configFilePathsFromTarget ?? 'N/A');
352
+ this.emit('log', 'configFilePathsFromTarget', configFilePathsFromTarget ?? 'N/A');
353
+ const configKey = this.#options?.config &&
354
+ this.#configProvider.set(mergeConfig(this.#options.config), undefined, this.#options.config);
355
+ configLog('option.config: %s', configKey ?? 'N/A');
356
+ this.emit('log', 'option.config', configFilePathsFromTarget ?? 'N/A');
357
+ let defaultRecommended = null;
358
+ if (!defaultConfigKey && !configFilePathsFromTarget && !configKey && !this.#options?.configFile) {
359
+ // No configured
360
+ // Default: set recommended
361
+ defaultRecommended = this.#configProvider.set(RECOMMENDED_CONFIG);
362
+ }
363
+ configLog('defaultRecommended: %s', defaultRecommended ?? 'N/A');
364
+ this.emit('log', 'defaultRecommended', defaultRecommended ?? 'N/A');
365
+ return this.#configProvider.resolve(this.#file, [configFilePathsFromTarget, this.#options?.configFile, configKey, defaultRecommended], cache);
366
+ });
367
+ // Rewrite deprecated rule names (v5 rule-system redesign, #3989) to
368
+ // their current replacement(s) so old configurations keep working.
369
+ // Applied once, here, after `extends` is fully merged — everything
370
+ // downstream (Ruleset, rule resolution, `--show-config`) sees only
371
+ // current rule names. Covers all three places a rule name can appear:
372
+ // the top-level `rules` map, and each `nodeRules`/`childNodeRules`
373
+ // entry's own `rules`.
374
+ const { config: aliasedConfig, warnings: ruleAliasWarnings } = applyRuleAliasesToConfig(resolvedConfigSet.config, ruleAliasTable);
375
+ // Kept structured (not folded into `errs` as generic `Error`s) so
376
+ // `MLCore` can report these under their own `rule-deprecation` ruleId,
377
+ // separate from genuine config-validation failures — see
378
+ // `ResolvedConfigSet`'s doc comment.
379
+ const configSet = ruleAliasWarnings.length === 0
380
+ ? { ...resolvedConfigSet, ruleDeprecations: [] }
381
+ : { ...resolvedConfigSet, config: aliasedConfig, ruleDeprecations: ruleAliasWarnings };
321
382
  this.emit('config', this.#file.path, configSet);
322
383
  if (this.#options?.watch) {
323
384
  // It doesn't watch the main HTML file because it may is watched and managed by a language server or text editor or more.
@@ -335,10 +396,22 @@ export class MLEngine extends Emitter {
335
396
  }
336
397
  async #resolvePretenders(
337
398
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
338
- configSet) {
339
- const pretenders = await resolvePretenders(configSet.config.pretenders);
340
- fileLog('Resolved pretenders: %O', pretenders);
341
- return pretenders;
399
+ configSet, cache) {
400
+ if (!cache) {
401
+ // A cache-busting re-resolve (e.g. watch mode after a file change) must also
402
+ // invalidate `@markuplint/pretenders`' own module-level resolution caches —
403
+ // otherwise a renamed export or a newly valid tsconfig `paths` alias keeps
404
+ // resolving as it did before the change for the rest of the process's lifetime.
405
+ await invalidatePretenderResolutionCaches();
406
+ }
407
+ const sourceCode = await this.#file.getCode();
408
+ const pretenders = await resolvePretenders(configSet.config.pretenders, {
409
+ filePath: this.#file.path,
410
+ sourceCode,
411
+ });
412
+ const disambiguated = await disambiguatePretendersForFile(this.#file.path, sourceCode, pretenders);
413
+ fileLog('Resolved pretenders: %O', disambiguated);
414
+ return disambiguated;
342
415
  }
343
416
  async #resolveRules(plugins, ruleset) {
344
417
  const rules = await resolveRules(plugins, ruleset, this.#options?.importPresetRules ?? true);
@@ -1,13 +1,5 @@
1
1
  import type { ReadonlyDeep } from 'type-fest';
2
- /**
3
- * Help text displayed when the CLI is invoked with `--help` or without arguments.
4
- * Documents all available options, flags, and usage examples.
5
- */
6
- export declare const help = "\nUsage\n\t$ markuplint <HTML file paths (glob format)>\n\t$ <stdout> | markuplint\n\nOptions\n\t--config, -c FILE_PATH A configuration file path.\n\t--fix, Fix HTML.\n\t--fix-dry-run Show what --fix would change without writing files.\n\t--format, -f FORMAT Output format. Support \"JSON\", \"Simple\", \"GitHub\" and \"Standard\". Default: \"Standard\".\n\t--no-search-config No search a configure file automatically.\n\t--ignore-ext Evaluate files that are received even though the type of extension.\n\t--no-import-preset-rules No import preset rules.\n\t--locale Locale of the message of violation. Default is an OS setting.\n\t--no-color, Output no color.\n\t--problem-only, -p Output only problems, without passeds.\n\t--no-allow-warnings Return status code 1 even if there are warnings.\n\t--allow-empty-input Return status code 1 even if there are no input files.\n\t--show-config Output computed configuration of the target file. Supports \"details\" and empty. Default: empty.\n\t--verbose Output with detailed information.\n\t--include-node-modules Include files in node_modules directory. Default: false.\n\t--severity-parse-error Specifies the severity level of parse errors. Supports \"error\", \"warning\", and \"off\". Default: \"error\".\n\t--max-count Limit the number of violations shown. Default: 0 (no limit).\n\t--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).\n\t--progressive-output Output results immediately after processing each file. Default: false.\n\n\t--suppress [Experimental] Generate/update suppressions file for all current errors.\n\t--suppress-rule RULE_ID [Experimental] Suppress only the specified rule.\n\t--prune-suppressions [Experimental] Remove stale entries from the suppressions file.\n\t--suppressions-location PATH [Experimental] Custom path for the suppressions file. Default: \"markuplint-suppressions.json\".\n\n\t--init Initialize settings interactively.\n\t--search Search lines of codes that include the target element by selectors.\n\n\t--help, -h Show help.\n\t--version, -v Show version.\n\nExamples\n\t$ markuplint verifyee.html --config path/to/.markuplintrc\n\t$ cat verifyee.html | markuplint\n";
7
- /**
8
- * The parsed CLI instance created by `meow`, providing access to
9
- * positional arguments (`cli.input`) and parsed flags (`cli.flags`).
10
- */
2
+ export declare const help = "\nUsage\n\t$ markuplint <HTML file paths (glob format)>\n\t$ <stdout> | markuplint\n\nOptions\n\t--config, -c FILE_PATH A configuration file path.\n\t--fix, Fix HTML.\n\t--fix-dry-run Show what --fix would change without writing files.\n\t--format, -f FORMAT Output format. Support \"JSON\", \"Simple\", \"GitHub\" and \"Standard\". Default: \"Standard\".\n\t--no-search-config No search a configure file automatically.\n\t--ignore-ext Evaluate files that are received even though the type of extension.\n\t--no-import-preset-rules No import preset rules.\n\t--locale Locale of the message of violation. Default is an OS setting.\n\t--no-color, Output no color.\n\t--problem-only, -p Output only problems, without passeds.\n\t--no-allow-warnings Return status code 1 even if there are warnings.\n\t--allow-empty-input Return status code 1 even if there are no input files.\n\t--show-config Output computed configuration of the target file. Supports \"details\" and empty. Default: empty.\n\t--verbose Output with detailed information.\n\t--include-node-modules Include files in node_modules directory. Default: false.\n\t--severity-parse-error Severity for the built-in parse-error channel. Supports \"error\", \"warning\", and \"off\". Unset by default: fatal ParserErrors emit at \"error\" and non-fatal parse5 events are off (opt-in per code via config).\n\t--severity-deprecation Severity for the built-in rule-deprecation channel (deprecated rule names). Supports \"error\", \"warning\", and \"off\". Default: \"warning\".\n\t--max-count Limit the number of violations shown. Default: 0 (no limit).\n\t--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).\n\t--no-progressive-output Wait until every file is processed before outputting results. Default: false (output progressively).\n\n\t--suppress [Experimental] Generate/update suppressions file for all current errors.\n\t--suppress-rule RULE_ID [Experimental] Suppress only the specified rule.\n\t--prune-suppressions [Experimental] Remove stale entries from the suppressions file.\n\t--suppressions-location PATH [Experimental] Custom path for the suppressions file. Default: \"markuplint-suppressions.json\".\n\n\t--init Initialize settings interactively.\n\t--search Search lines of codes that include the target element by selectors.\n\n\t--help, -h Show help.\n\t--version, -v Show version.\n\nExamples\n\t$ markuplint verifyee.html --config path/to/.markuplintrc\n\t$ cat verifyee.html | markuplint\n";
11
3
  export declare const cli: import("meow").Result<{
12
4
  config: {
13
5
  type: "string";
@@ -81,7 +73,9 @@ export declare const cli: import("meow").Result<{
81
73
  };
82
74
  severityParseError: {
83
75
  type: "string";
84
- default: string;
76
+ };
77
+ severityDeprecation: {
78
+ type: "string";
85
79
  };
86
80
  maxCount: {
87
81
  type: "number";
@@ -93,7 +87,7 @@ export declare const cli: import("meow").Result<{
93
87
  };
94
88
  progressiveOutput: {
95
89
  type: "boolean";
96
- default: false;
90
+ default: true;
97
91
  };
98
92
  suppress: {
99
93
  type: "boolean";
@@ -110,8 +104,4 @@ export declare const cli: import("meow").Result<{
110
104
  type: "string";
111
105
  };
112
106
  }>;
113
- /**
114
- * Deeply read-only type representing the parsed CLI flags.
115
- * Derived from the `meow` flag definitions in {@link cli}.
116
- */
117
107
  export type CLIOptions = ReadonlyDeep<typeof cli.flags>;
@@ -1,8 +1,4 @@
1
1
  import meow from 'meow';
2
- /**
3
- * Help text displayed when the CLI is invoked with `--help` or without arguments.
4
- * Documents all available options, flags, and usage examples.
5
- */
6
2
  export const help = `
7
3
  Usage
8
4
  $ markuplint <HTML file paths (glob format)>
@@ -24,10 +20,11 @@ Options
24
20
  --show-config Output computed configuration of the target file. Supports "details" and empty. Default: empty.
25
21
  --verbose Output with detailed information.
26
22
  --include-node-modules Include files in node_modules directory. Default: false.
27
- --severity-parse-error Specifies the severity level of parse errors. Supports "error", "warning", and "off". Default: "error".
23
+ --severity-parse-error Severity for the built-in parse-error channel. Supports "error", "warning", and "off". Unset by default: fatal ParserErrors emit at "error" and non-fatal parse5 events are off (opt-in per code via config).
24
+ --severity-deprecation Severity for the built-in rule-deprecation channel (deprecated rule names). Supports "error", "warning", and "off". Default: "warning".
28
25
  --max-count Limit the number of violations shown. Default: 0 (no limit).
29
26
  --max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).
30
- --progressive-output Output results immediately after processing each file. Default: false.
27
+ --no-progressive-output Wait until every file is processed before outputting results. Default: false (output progressively).
31
28
 
32
29
  --suppress [Experimental] Generate/update suppressions file for all current errors.
33
30
  --suppress-rule RULE_ID [Experimental] Suppress only the specified rule.
@@ -44,10 +41,6 @@ Examples
44
41
  $ markuplint verifyee.html --config path/to/.markuplintrc
45
42
  $ cat verifyee.html | markuplint
46
43
  `;
47
- /**
48
- * The parsed CLI instance created by `meow`, providing access to
49
- * positional arguments (`cli.input`) and parsed flags (`cli.flags`).
50
- */
51
44
  export const cli = meow(help, {
52
45
  importMeta: import.meta,
53
46
  flags: {
@@ -123,7 +116,9 @@ export const cli = meow(help, {
123
116
  },
124
117
  severityParseError: {
125
118
  type: 'string',
126
- default: 'error',
119
+ },
120
+ severityDeprecation: {
121
+ type: 'string',
127
122
  },
128
123
  maxCount: {
129
124
  type: 'number',
@@ -135,8 +130,7 @@ export const cli = meow(help, {
135
130
  },
136
131
  progressiveOutput: {
137
132
  type: 'boolean',
138
- // TODO: It will be changed to `true` in the next major version.
139
- default: false,
133
+ default: true,
140
134
  },
141
135
  suppress: {
142
136
  type: 'boolean',
@@ -1,16 +1,4 @@
1
1
  import type { CLIOptions } from './bootstrap.js';
2
2
  import type { APIOptions } from '../api/types.js';
3
3
  import type { Target } from '@markuplint/file-resolver';
4
- /**
5
- * Executes the markuplint linting command against the given files.
6
- *
7
- * Resolves file targets, creates an {@link MLEngine} for each file, collects
8
- * violations, and outputs results in the requested format. When the `--fix`
9
- * flag is set, overwrites files with their auto-fixed content.
10
- *
11
- * @param files - The list of file targets (paths or inline source code) to lint.
12
- * @param options - CLI options controlling output format, fix mode, locale, and other behaviors.
13
- * @param apiOptions - Optional overrides for the underlying API (e.g., custom rules or config).
14
- * @returns `true` if any errors were found (or warnings exceeded the limit), `false` otherwise.
15
- */
16
4
  export declare function command(files: readonly Readonly<Target>[], options: CLIOptions, apiOptions?: APIOptions): Promise<boolean>;