markuplint 5.0.0-rc.5 → 5.0.0-rc.7
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 +46 -0
- package/README.md +1 -0
- package/lib/api/lint.d.ts +5 -0
- package/lib/api/lint.js +40 -3
- package/lib/api/ml-engine.d.ts +44 -2
- package/lib/api/ml-engine.js +58 -32
- package/lib/cli/bootstrap.d.ts +4 -1
- package/lib/cli/bootstrap.js +4 -0
- package/lib/cli/command.js +40 -28
- package/lib/dedupe-config-violations.d.ts +29 -0
- package/lib/dedupe-config-violations.js +41 -0
- package/package.json +13 -13
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
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.7](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.6...v5.0.0-rc.7) (2026-08-31)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **markuplint:** surface appliedOverrides in --show-config=details ([10ed615](https://github.com/markuplint/markuplint/commit/10ed6157cf99786971ae90551ea0411cecdc8433))
|
|
11
|
+
|
|
12
|
+
# [5.0.0-rc.6](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.5...v5.0.0-rc.6) (2026-08-30)
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
- 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))
|
|
17
|
+
- 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)
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- split rule-deprecation notices out of config-error ([#4013](https://github.com/markuplint/markuplint/issues/4013)) ([812e6f3](https://github.com/markuplint/markuplint/commit/812e6f356839af8f257cfd91e6b16cfdfdd7cf33))
|
|
22
|
+
|
|
23
|
+
### Performance Improvements
|
|
24
|
+
|
|
25
|
+
- 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)
|
|
26
|
+
|
|
27
|
+
### BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
- `ConfigProvider#resolve(targetFile, names, false)` no longer
|
|
30
|
+
clears the provider's store/cache/plugin-resolution caches by itself. Callers
|
|
31
|
+
that relied on `cache: false` alone to force a fresh re-read must now call
|
|
32
|
+
the new `ConfigProvider#invalidate()` first.
|
|
33
|
+
- violations for deprecated rule names now have
|
|
34
|
+
`ruleId: 'rule-deprecation'` instead of `ruleId: 'config-error'`. Any
|
|
35
|
+
consumer filtering `MLCore.verify()` output (or the markuplint CLI/API) by
|
|
36
|
+
`ruleId === 'config-error'` to catch deprecation messages must also check
|
|
37
|
+
for `rule-deprecation`.
|
|
38
|
+
|
|
39
|
+
- feat(markuplint): add --severity-deprecation CLI flag
|
|
40
|
+
|
|
41
|
+
Wires the new severity.deprecation config option (@markuplint/ml-config)
|
|
42
|
+
and the rule-deprecation ruleId (@markuplint/ml-core) through the CLI:
|
|
43
|
+
|
|
44
|
+
- --severity-deprecation flag, mirroring --severity-parse-error
|
|
45
|
+
- --show-config details now also surfaces ruleDeprecations
|
|
46
|
+
- per-run dedupe and failed-file counting generalized to cover both
|
|
47
|
+
config-level ruleIds (config-error and rule-deprecation), not just
|
|
48
|
+
config-error
|
|
49
|
+
|
|
50
|
+
* docs(website): document severity.deprecation (EN + JA)
|
|
51
|
+
|
|
6
52
|
# [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
|
|
7
53
|
|
|
8
54
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -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.
|
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
|
-
|
|
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
|
}
|
package/lib/api/ml-engine.d.ts
CHANGED
|
@@ -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.
|
|
@@ -86,6 +128,6 @@ export declare class MLEngine extends Emitter<MLEngineEventMap> {
|
|
|
86
128
|
* @param cache - Whether to reuse previously loaded config files
|
|
87
129
|
* @returns The resolved configuration set
|
|
88
130
|
*/
|
|
89
|
-
resolveConfig(cache: boolean): Promise<
|
|
131
|
+
resolveConfig(cache: boolean): Promise<ResolvedConfigSet>;
|
|
90
132
|
}
|
|
91
133
|
export {};
|
package/lib/api/ml-engine.js
CHANGED
|
@@ -10,6 +10,15 @@ import { i18n } from '../i18n.js';
|
|
|
10
10
|
const log = coreLog.extend('ml-engine');
|
|
11
11
|
const fileLog = log.extend('file');
|
|
12
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'] };
|
|
13
22
|
/**
|
|
14
23
|
* The main markuplint engine that orchestrates file resolution, configuration loading,
|
|
15
24
|
* parsing, and linting. Supports both single-file and watch-mode operation.
|
|
@@ -63,7 +72,7 @@ export class MLEngine extends Emitter {
|
|
|
63
72
|
}
|
|
64
73
|
this.#file = file;
|
|
65
74
|
this.#options = options;
|
|
66
|
-
this.#configProvider = new ConfigProvider();
|
|
75
|
+
this.#configProvider = options?.configProvider ?? new ConfigProvider();
|
|
67
76
|
this.watchMode(!!this.#options?.watch);
|
|
68
77
|
log('[MLEngine] Initialized: %s', this.#file.path);
|
|
69
78
|
}
|
|
@@ -225,6 +234,7 @@ export class MLEngine extends Emitter {
|
|
|
225
234
|
plugins: [],
|
|
226
235
|
files: new Set(),
|
|
227
236
|
errs: [error],
|
|
237
|
+
ruleDeprecations: [],
|
|
228
238
|
};
|
|
229
239
|
}
|
|
230
240
|
else {
|
|
@@ -292,6 +302,7 @@ export class MLEngine extends Emitter {
|
|
|
292
302
|
locale,
|
|
293
303
|
ruleCommonSettings,
|
|
294
304
|
configErrors: configSet.errs,
|
|
305
|
+
ruleDeprecations: configSet.ruleDeprecations,
|
|
295
306
|
};
|
|
296
307
|
}
|
|
297
308
|
/**
|
|
@@ -313,28 +324,46 @@ export class MLEngine extends Emitter {
|
|
|
313
324
|
async resolveConfig(cache) {
|
|
314
325
|
this.emit('log', 'resolveConfig', JSON.stringify(this.#configProvider, null, 2));
|
|
315
326
|
configLog('configProvider: %s', this.#configProvider);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
});
|
|
338
367
|
// Rewrite deprecated rule names (v5 rule-system redesign, #3989) to
|
|
339
368
|
// their current replacement(s) so old configurations keep working.
|
|
340
369
|
// Applied once, here, after `extends` is fully merged — everything
|
|
@@ -343,16 +372,13 @@ export class MLEngine extends Emitter {
|
|
|
343
372
|
// the top-level `rules` map, and each `nodeRules`/`childNodeRules`
|
|
344
373
|
// entry's own `rules`.
|
|
345
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.
|
|
346
379
|
const configSet = ruleAliasWarnings.length === 0
|
|
347
|
-
? resolvedConfigSet
|
|
348
|
-
: {
|
|
349
|
-
...resolvedConfigSet,
|
|
350
|
-
config: aliasedConfig,
|
|
351
|
-
errs: [
|
|
352
|
-
...resolvedConfigSet.errs,
|
|
353
|
-
...ruleAliasWarnings.map(({ deprecatedName, replacedBy }) => new Error(`Rule "${deprecatedName}" is deprecated and will be removed in v6. Use ${replacedBy.join(', ')} instead.`)),
|
|
354
|
-
],
|
|
355
|
-
};
|
|
380
|
+
? { ...resolvedConfigSet, ruleDeprecations: [] }
|
|
381
|
+
: { ...resolvedConfigSet, config: aliasedConfig, ruleDeprecations: ruleAliasWarnings };
|
|
356
382
|
this.emit('config', this.#file.path, configSet);
|
|
357
383
|
if (this.#options?.watch) {
|
|
358
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.
|
package/lib/cli/bootstrap.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReadonlyDeep } from 'type-fest';
|
|
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--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";
|
|
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";
|
|
3
3
|
export declare const cli: import("meow").Result<{
|
|
4
4
|
config: {
|
|
5
5
|
type: "string";
|
|
@@ -74,6 +74,9 @@ export declare const cli: import("meow").Result<{
|
|
|
74
74
|
severityParseError: {
|
|
75
75
|
type: "string";
|
|
76
76
|
};
|
|
77
|
+
severityDeprecation: {
|
|
78
|
+
type: "string";
|
|
79
|
+
};
|
|
77
80
|
maxCount: {
|
|
78
81
|
type: "number";
|
|
79
82
|
default: number;
|
package/lib/cli/bootstrap.js
CHANGED
|
@@ -21,6 +21,7 @@ Options
|
|
|
21
21
|
--verbose Output with detailed information.
|
|
22
22
|
--include-node-modules Include files in node_modules directory. Default: false.
|
|
23
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".
|
|
24
25
|
--max-count Limit the number of violations shown. Default: 0 (no limit).
|
|
25
26
|
--max-warnings Number of warnings to trigger nonzero exit code. Default: -1 (no limit).
|
|
26
27
|
--no-progressive-output Wait until every file is processed before outputting results. Default: false (output progressively).
|
|
@@ -116,6 +117,9 @@ export const cli = meow(help, {
|
|
|
116
117
|
severityParseError: {
|
|
117
118
|
type: 'string',
|
|
118
119
|
},
|
|
120
|
+
severityDeprecation: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
},
|
|
119
123
|
maxCount: {
|
|
120
124
|
type: 'number',
|
|
121
125
|
default: 0,
|
package/lib/cli/command.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { resolveFiles } from '@markuplint/file-resolver';
|
|
3
|
+
import { ConfigProvider, resolveFiles } from '@markuplint/file-resolver';
|
|
4
4
|
import { ViolationCollector } from '@markuplint/ml-core';
|
|
5
5
|
import { isFatalError } from '@markuplint/shared';
|
|
6
6
|
import { MLEngine } from '../api/index.js';
|
|
7
7
|
import { log } from '../debug.js';
|
|
8
8
|
import { applySuppressions, generateSuppressions, mergeSuppressions, pruneSuppressions, readSuppressionsFile, resolveSuppressionsPath, writeSuppressionsFile, } from '../suppressions/index.js';
|
|
9
|
+
import { CONFIG_LEVEL_RULE_IDS, dedupeConfigLevelViolations } from '../dedupe-config-violations.js';
|
|
9
10
|
import { outputDryRunDiff } from './dry-run-output.js';
|
|
10
11
|
import { output, outputSummary } from './output.js';
|
|
12
|
+
/**
|
|
13
|
+
* Validates a `--severity-*` flag's raw string value against the uniform
|
|
14
|
+
* single-value form both `--severity-parse-error` and `--severity-deprecation`
|
|
15
|
+
* accept, shared so adding another such flag doesn't mean copy-pasting this
|
|
16
|
+
* check again.
|
|
17
|
+
*
|
|
18
|
+
* @returns the validated value, or `undefined` if unset or not one of
|
|
19
|
+
* `"error"`, `"warning"`, `"off"`.
|
|
20
|
+
*/
|
|
21
|
+
function parseSeverityFlag(value) {
|
|
22
|
+
const normalized = value?.toLowerCase();
|
|
23
|
+
if (normalized != null && ['error', 'warning', 'off'].includes(normalized)) {
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
11
28
|
export async function command(files, options, apiOptions) {
|
|
12
29
|
const fixDryRun = options.fixDryRun;
|
|
13
30
|
if (options.fix && fixDryRun) {
|
|
@@ -47,18 +64,22 @@ export async function command(files, options, apiOptions) {
|
|
|
47
64
|
const collector = new ViolationCollector(options.maxCount);
|
|
48
65
|
const processedFiles = [];
|
|
49
66
|
const skippedFiles = [];
|
|
50
|
-
// `
|
|
51
|
-
//
|
|
52
|
-
// message is identical across every file. Track messages already
|
|
53
|
-
// reported once so a run over many files doesn't repeat the same lines
|
|
54
|
-
// per file — the config is one thing, not N things.
|
|
67
|
+
// See `dedupeConfigLevelViolations` for why this Set needs to persist
|
|
68
|
+
// across the whole run (not per file).
|
|
55
69
|
const seenConfigMessages = new Set();
|
|
56
70
|
const filesContent = new Map();
|
|
57
71
|
const engines = new Map();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
// Shared across every file in this run so its config cache — keyed by
|
|
73
|
+
// resolved config `names`, not by target file — actually helps: config
|
|
74
|
+
// loading/merging/plugin-resolution is done once per distinct config,
|
|
75
|
+
// not once per file. See #3997.
|
|
76
|
+
const configProvider = new ConfigProvider();
|
|
77
|
+
const parsedSeverityParseError = parseSeverityFlag(options.severityParseError);
|
|
78
|
+
const parsedSeverityDeprecation = parseSeverityFlag(options.severityDeprecation);
|
|
79
|
+
const severity = {
|
|
80
|
+
...(parsedSeverityParseError != null && { parseError: parsedSeverityParseError }),
|
|
81
|
+
...(parsedSeverityDeprecation != null && { deprecation: parsedSeverityDeprecation }),
|
|
82
|
+
};
|
|
62
83
|
// Progressive output prints each file's own violations as soon as that
|
|
63
84
|
// file is processed, ahead of the two whole-run passes that batch output
|
|
64
85
|
// waits for: suppressions (applied once at the end, below, via
|
|
@@ -101,6 +122,7 @@ export async function command(files, options, apiOptions) {
|
|
|
101
122
|
debug: verbose,
|
|
102
123
|
severity,
|
|
103
124
|
...apiOptions,
|
|
125
|
+
configProvider,
|
|
104
126
|
});
|
|
105
127
|
if (options.showConfig != null) {
|
|
106
128
|
const isDetails = options.showConfig === 'details';
|
|
@@ -116,6 +138,8 @@ export async function command(files, options, apiOptions) {
|
|
|
116
138
|
dependencies,
|
|
117
139
|
plugins: configSet.plugins,
|
|
118
140
|
errors: configSet.errs,
|
|
141
|
+
ruleDeprecations: configSet.ruleDeprecations,
|
|
142
|
+
appliedOverrides: configSet.appliedOverrides ?? [],
|
|
119
143
|
};
|
|
120
144
|
}
|
|
121
145
|
else {
|
|
@@ -143,20 +167,7 @@ export async function command(files, options, apiOptions) {
|
|
|
143
167
|
const violationsBeforeDedupe = fixDryRun
|
|
144
168
|
? result.violations
|
|
145
169
|
: (result.fixSummary?.finalPassViolations ?? result.violations);
|
|
146
|
-
|
|
147
|
-
// not shared across a run), so an identical message would otherwise
|
|
148
|
-
// repeat once per file. Keep only the first occurrence across this run.
|
|
149
|
-
const reportedViolations = violationsBeforeDedupe.filter(violation => {
|
|
150
|
-
if (violation.ruleId !== 'config-error') {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
const key = `${violation.ruleId} ${violation.message}`;
|
|
154
|
-
if (seenConfigMessages.has(key)) {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
seenConfigMessages.add(key);
|
|
158
|
-
return true;
|
|
159
|
-
});
|
|
170
|
+
const reportedViolations = dedupeConfigLevelViolations(violationsBeforeDedupe, seenConfigMessages);
|
|
160
171
|
// Progressive出力が有効でJSON形式でない場合
|
|
161
172
|
if (progressiveOutput && format !== 'json') {
|
|
162
173
|
// 即座に出力
|
|
@@ -326,10 +337,11 @@ export async function command(files, options, apiOptions) {
|
|
|
326
337
|
let failedFileCount = 0;
|
|
327
338
|
for (const filePath of processedFiles) {
|
|
328
339
|
const violations = outputViolationsByFile.get(filePath) || [];
|
|
329
|
-
// A file whose only violations are
|
|
330
|
-
// own content — the config issue is reported once
|
|
331
|
-
// (see the dedupe above), not attributable to this
|
|
332
|
-
|
|
340
|
+
// A file whose only violations are config-level (`CONFIG_LEVEL_RULE_IDS`)
|
|
341
|
+
// didn't fail on its own content — the config issue is reported once
|
|
342
|
+
// for the whole run (see the dedupe above), not attributable to this
|
|
343
|
+
// particular file.
|
|
344
|
+
if (violations.some(violation => !CONFIG_LEVEL_RULE_IDS.has(violation.ruleId))) {
|
|
333
345
|
failedFileCount++;
|
|
334
346
|
}
|
|
335
347
|
for (const violation of violations) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Violation } from '@markuplint/ml-config';
|
|
2
|
+
/**
|
|
3
|
+
* Synthetic ruleIds produced from resolving a run's config rather than from
|
|
4
|
+
* linting a file's content (`config-error`: broken config; `rule-deprecation`:
|
|
5
|
+
* deprecated-but-working rule names). Both regenerate identically for every
|
|
6
|
+
* file that shares the same config, so both need the same per-run dedupe —
|
|
7
|
+
* see {@link dedupeConfigLevelViolations} — and the same exclusion from
|
|
8
|
+
* per-file failure counting (`packages/markuplint/src/cli/command.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export declare const CONFIG_LEVEL_RULE_IDS: Set<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Filters `violations` down to the first occurrence of each distinct
|
|
13
|
+
* config-level ({@link CONFIG_LEVEL_RULE_IDS}) message, tracked in `seen`
|
|
14
|
+
* across calls. Non-config-level violations always pass through unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Even with config resolution now shared across a run (see
|
|
17
|
+
* `ConfigProvider`, #3997), each file's `MLCore.verify()` still builds its
|
|
18
|
+
* own `violations` array independently from that shared config data — so a
|
|
19
|
+
* config-level message would otherwise still repeat once per file that
|
|
20
|
+
* shares the config. The config is one thing, not N things. `seen` is
|
|
21
|
+
* caller-owned (a plain `Set<string>`) so both the CLI (per run) and the
|
|
22
|
+
* `lint()` API (per call) can each keep their own dedupe scope without
|
|
23
|
+
* sharing state between unrelated runs.
|
|
24
|
+
*
|
|
25
|
+
* @param violations - Violations to filter, in file-processing order
|
|
26
|
+
* @param seen - Accumulates `${ruleId} ${message}` keys already kept; mutated in place
|
|
27
|
+
* @returns `violations` with repeat config-level entries removed
|
|
28
|
+
*/
|
|
29
|
+
export declare function dedupeConfigLevelViolations(violations: readonly Violation[], seen: Set<string>): readonly Violation[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CONFIG_ERROR_RULE_ID, RULE_DEPRECATION_RULE_ID } from '@markuplint/ml-core';
|
|
2
|
+
/**
|
|
3
|
+
* Synthetic ruleIds produced from resolving a run's config rather than from
|
|
4
|
+
* linting a file's content (`config-error`: broken config; `rule-deprecation`:
|
|
5
|
+
* deprecated-but-working rule names). Both regenerate identically for every
|
|
6
|
+
* file that shares the same config, so both need the same per-run dedupe —
|
|
7
|
+
* see {@link dedupeConfigLevelViolations} — and the same exclusion from
|
|
8
|
+
* per-file failure counting (`packages/markuplint/src/cli/command.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export const CONFIG_LEVEL_RULE_IDS = new Set([CONFIG_ERROR_RULE_ID, RULE_DEPRECATION_RULE_ID]);
|
|
11
|
+
/**
|
|
12
|
+
* Filters `violations` down to the first occurrence of each distinct
|
|
13
|
+
* config-level ({@link CONFIG_LEVEL_RULE_IDS}) message, tracked in `seen`
|
|
14
|
+
* across calls. Non-config-level violations always pass through unchanged.
|
|
15
|
+
*
|
|
16
|
+
* Even with config resolution now shared across a run (see
|
|
17
|
+
* `ConfigProvider`, #3997), each file's `MLCore.verify()` still builds its
|
|
18
|
+
* own `violations` array independently from that shared config data — so a
|
|
19
|
+
* config-level message would otherwise still repeat once per file that
|
|
20
|
+
* shares the config. The config is one thing, not N things. `seen` is
|
|
21
|
+
* caller-owned (a plain `Set<string>`) so both the CLI (per run) and the
|
|
22
|
+
* `lint()` API (per call) can each keep their own dedupe scope without
|
|
23
|
+
* sharing state between unrelated runs.
|
|
24
|
+
*
|
|
25
|
+
* @param violations - Violations to filter, in file-processing order
|
|
26
|
+
* @param seen - Accumulates `${ruleId} ${message}` keys already kept; mutated in place
|
|
27
|
+
* @returns `violations` with repeat config-level entries removed
|
|
28
|
+
*/
|
|
29
|
+
export function dedupeConfigLevelViolations(violations, seen) {
|
|
30
|
+
return violations.filter(violation => {
|
|
31
|
+
if (!CONFIG_LEVEL_RULE_IDS.has(violation.ruleId)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const key = `${violation.ruleId} ${violation.message}`;
|
|
35
|
+
if (seen.has(key)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
seen.add(key);
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markuplint",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.7",
|
|
4
4
|
"description": "An HTML linter for all markup developers",
|
|
5
5
|
"author": "Yusuke Hirao",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,17 +35,17 @@
|
|
|
35
35
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@markuplint/cli-utils": "5.0.0-rc.
|
|
39
|
-
"@markuplint/file-resolver": "5.0.0-rc.
|
|
40
|
-
"@markuplint/html-parser": "5.0.0-rc.
|
|
41
|
-
"@markuplint/html-spec": "5.0.0-rc.
|
|
42
|
-
"@markuplint/i18n": "5.0.0-rc.
|
|
43
|
-
"@markuplint/ml-ast": "5.0.0-rc.
|
|
44
|
-
"@markuplint/ml-config": "5.0.0-rc.
|
|
45
|
-
"@markuplint/ml-core": "5.0.0-rc.
|
|
46
|
-
"@markuplint/ml-spec": "5.0.0-rc.
|
|
47
|
-
"@markuplint/rules": "5.0.0-rc.
|
|
48
|
-
"@markuplint/shared": "5.0.0-rc.
|
|
38
|
+
"@markuplint/cli-utils": "5.0.0-rc.7",
|
|
39
|
+
"@markuplint/file-resolver": "5.0.0-rc.7",
|
|
40
|
+
"@markuplint/html-parser": "5.0.0-rc.7",
|
|
41
|
+
"@markuplint/html-spec": "5.0.0-rc.7",
|
|
42
|
+
"@markuplint/i18n": "5.0.0-rc.7",
|
|
43
|
+
"@markuplint/ml-ast": "5.0.0-rc.7",
|
|
44
|
+
"@markuplint/ml-config": "5.0.0-rc.7",
|
|
45
|
+
"@markuplint/ml-core": "5.0.0-rc.7",
|
|
46
|
+
"@markuplint/ml-spec": "5.0.0-rc.7",
|
|
47
|
+
"@markuplint/rules": "5.0.0-rc.7",
|
|
48
|
+
"@markuplint/shared": "5.0.0-rc.7",
|
|
49
49
|
"@types/debug": "4.1.13",
|
|
50
50
|
"chokidar": "5.0.0",
|
|
51
51
|
"debug": "4.4.3",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"strip-ansi": "7.2.0",
|
|
56
56
|
"type-fest": "5.6.0"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "8d7a3b801e0f5f88438c003e7eee2bd45e6aedd0"
|
|
59
59
|
}
|