@markuplint/file-resolver 5.2.0 → 5.3.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,21 @@
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.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **file-resolver:** cache the auto-load promise so a mid-load cache clear is not undone ([1f1b40e](https://github.com/markuplint/markuplint/commit/1f1b40e4020dfe51310f73e7c89bc887a4ad04d4))
11
+ * **file-resolver:** pass the working directory to the lint-time disambiguation ([542cc62](https://github.com/markuplint/markuplint/commit/542cc6272ab8922116bda7d87bb4c9ccb984c3f1)), closes [#4269](https://github.com/markuplint/markuplint/issues/4269)
12
+ * **file-resolver:** resolve a lone scanned component against the file's imports ([c37e922](https://github.com/markuplint/markuplint/commit/c37e922eed92876dd963dd74e9990a4a0da68ccc))
13
+ * make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
14
+
15
+ ### Performance Improvements
16
+
17
+ * **file-resolver:** cache auto-load misses to skip repeated module resolution ([91931ca](https://github.com/markuplint/markuplint/commit/91931caf078c815c0e456dde51f0230b6d782c9d))
18
+ * **file-resolver:** group the config search debug calls under one guard ([0353e02](https://github.com/markuplint/markuplint/commit/0353e029f79471b8b5495a4ae83edea58ab8cfe3))
19
+
20
+
6
21
  # [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
7
22
 
8
23
  ### Bug Fixes
@@ -1,4 +1,5 @@
1
1
  import type { Ruleset, AnyMLRule } from '@markuplint/ml-core';
2
+ export declare function clearAutoLoadRulesCache(): void;
2
3
  export declare function autoLoadRules(ruleset: Ruleset): Promise<{
3
4
  rules: AnyMLRule[];
4
5
  errors: unknown[];
@@ -1,58 +1,53 @@
1
1
  import { MLRule } from '@markuplint/ml-core';
2
+ /**
3
+ * Keyed by rule name, holding the in-flight or settled load. A miss stays
4
+ * cached (`rule: null`, with the errors each probe raised) until
5
+ * {@link clearAutoLoadRulesCache}: this runs once per linted file and built-in
6
+ * rule names never resolve to a package, so re-probing would cost two failing
7
+ * imports per rule per file. Same lifetime policy as `generalImport`'s cache,
8
+ * which never retries a miss either.
9
+ *
10
+ * The entry is set synchronously before the first `await`, so a
11
+ * `clearAutoLoadRulesCache()` that lands mid-load evicts the promise and the
12
+ * eventual result is never written back into the fresh map.
13
+ */
2
14
  const cache = new Map();
15
+ export function clearAutoLoadRulesCache() {
16
+ cache.clear();
17
+ }
3
18
  export async function autoLoadRules(ruleset) {
4
19
  const rules = [];
5
20
  const errors = [];
6
21
  for (const ruleName of Object.keys(ruleset.rules)) {
7
- const cached = cache.get(ruleName);
8
- if (cached) {
9
- rules.push(cached);
10
- continue;
11
- }
12
- let seed = null;
13
- try {
14
- const _module = await import(`@markuplint/rule-${ruleName}`);
15
- seed = _module.default;
16
- if (!(seed && 'defaultValue' in seed && 'defaultOptions' in seed && 'verify' in seed)) {
17
- seed = null;
18
- }
19
- }
20
- catch (error) {
21
- errors.push(error);
22
+ let loading = cache.get(ruleName);
23
+ if (!loading) {
24
+ loading = load(ruleName);
25
+ cache.set(ruleName, loading);
22
26
  }
23
- if (seed) {
24
- const rule = new MLRule({
25
- name: ruleName,
26
- ...seed,
27
- });
28
- cache.set(ruleName, rule);
29
- rules.push(rule);
30
- continue;
27
+ const loaded = await loading;
28
+ if (loaded.rule) {
29
+ rules.push(loaded.rule);
31
30
  }
31
+ errors.push(...loaded.errors);
32
+ }
33
+ return { rules, errors };
34
+ }
35
+ async function load(ruleName) {
36
+ const errors = [];
37
+ for (const specifier of [`@markuplint/rule-${ruleName}`, `markuplint-rule-${ruleName}`]) {
32
38
  try {
33
- const _module = await import(`markuplint-rule-${ruleName}`);
34
- seed = _module.default;
35
- if (!(seed && 'defaultValue' in seed && 'defaultOptions' in seed && 'verify' in seed)) {
36
- seed = null;
39
+ const mod = await import(specifier);
40
+ const seed = mod.default;
41
+ if (isRuleSeed(seed)) {
42
+ return { rule: new MLRule({ name: ruleName, ...seed }), errors };
37
43
  }
38
44
  }
39
45
  catch (error) {
40
46
  errors.push(error);
41
47
  }
42
- if (seed) {
43
- const rule = new MLRule({
44
- name: ruleName,
45
- ...seed,
46
- });
47
- cache.set(ruleName, rule);
48
- rules.push(rule);
49
- continue;
50
- }
51
48
  }
52
- return {
53
- // Clone
54
- rules: [...rules],
55
- // Clone
56
- errors: [...errors],
57
- };
49
+ return { rule: null, errors };
50
+ }
51
+ function isRuleSeed(seed) {
52
+ return !!seed && typeof seed === 'object' && 'defaultValue' in seed && 'defaultOptions' in seed && 'verify' in seed;
58
53
  }
@@ -21,16 +21,19 @@ export declare class ConfigProvider {
21
21
  * Clears every cached and stored config entry: the base-config cache
22
22
  * (`#cache`), the loaded/registered config store (`#store`), `set()`'s
23
23
  * identity→key stabilization (`#autoKeys`), `resolve-plugins.ts`'s own
24
- * module-level plugin-resolution cache, and the shared `cosmiconfig`
25
- * explorer's own search/load caches (so {@link search}, called right
26
- * after this, re-reads the current file content instead of a stale
27
- * cosmiconfig-level cache).
24
+ * module-level plugin-resolution cache, `auto-load-rules.ts`'s rule
25
+ * cache, and the shared `cosmiconfig` explorer's own search/load caches
26
+ * (so {@link search}, called right after this, re-reads the current file
27
+ * content instead of a stale cosmiconfig-level cache).
28
28
  *
29
- * The `cosmiconfig` explorer and the `resolve-plugins.ts` cache are
30
- * module-level singletons shared by every `ConfigProvider` instance in
31
- * the process — clearing them here affects other instances too, not just
32
- * this one. Harmless (they just re-search/re-load), but worth knowing
33
- * when reasoning about a cache-busting re-resolve's blast radius.
29
+ * The `cosmiconfig` explorer and the `resolve-plugins.ts` /
30
+ * `auto-load-rules.ts` caches are module-level singletons shared by every
31
+ * `ConfigProvider` instance in the process — clearing them here affects
32
+ * other instances too, not just this one. Correctness is unaffected (they
33
+ * just re-search/re-load), but the auto-load cache holds a miss for every
34
+ * built-in rule name, so the first lint after a clear repeats two failing
35
+ * imports per rule — worth knowing when reasoning about a cache-busting
36
+ * re-resolve's blast radius.
34
37
  *
35
38
  * Callers doing a cache-busting re-resolve (e.g. watch mode after a file
36
39
  * change) must call this **before** registering any inline config via
@@ -3,6 +3,7 @@ import { mergeConfig } from '@markuplint/ml-config';
3
3
  import { ConfigParserError } from '@markuplint/parser-utils';
4
4
  import { InvalidSelectorError, createSelector } from '@markuplint/selector';
5
5
  import { nonNullableFilter, toNoEmptyStringArrayFromStringOrArray, ConfigLoadError } from '@markuplint/shared';
6
+ import { clearAutoLoadRulesCache } from './auto-load-rules.js';
6
7
  import { load as loadConfig, search, clearExplorerCache } from './cosmiconfig.js';
7
8
  import { log } from './debug.js';
8
9
  import { generalImport } from './general-import.js';
@@ -52,16 +53,19 @@ export class ConfigProvider {
52
53
  * Clears every cached and stored config entry: the base-config cache
53
54
  * (`#cache`), the loaded/registered config store (`#store`), `set()`'s
54
55
  * identity→key stabilization (`#autoKeys`), `resolve-plugins.ts`'s own
55
- * module-level plugin-resolution cache, and the shared `cosmiconfig`
56
- * explorer's own search/load caches (so {@link search}, called right
57
- * after this, re-reads the current file content instead of a stale
58
- * cosmiconfig-level cache).
56
+ * module-level plugin-resolution cache, `auto-load-rules.ts`'s rule
57
+ * cache, and the shared `cosmiconfig` explorer's own search/load caches
58
+ * (so {@link search}, called right after this, re-reads the current file
59
+ * content instead of a stale cosmiconfig-level cache).
59
60
  *
60
- * The `cosmiconfig` explorer and the `resolve-plugins.ts` cache are
61
- * module-level singletons shared by every `ConfigProvider` instance in
62
- * the process — clearing them here affects other instances too, not just
63
- * this one. Harmless (they just re-search/re-load), but worth knowing
64
- * when reasoning about a cache-busting re-resolve's blast radius.
61
+ * The `cosmiconfig` explorer and the `resolve-plugins.ts` /
62
+ * `auto-load-rules.ts` caches are module-level singletons shared by every
63
+ * `ConfigProvider` instance in the process — clearing them here affects
64
+ * other instances too, not just this one. Correctness is unaffected (they
65
+ * just re-search/re-load), but the auto-load cache holds a miss for every
66
+ * built-in rule name, so the first lint after a clear repeats two failing
67
+ * imports per rule — worth knowing when reasoning about a cache-busting
68
+ * re-resolve's blast radius.
65
69
  *
66
70
  * Callers doing a cache-busting re-resolve (e.g. watch mode after a file
67
71
  * change) must call this **before** registering any inline config via
@@ -77,6 +81,7 @@ export class ConfigProvider {
77
81
  this.#cache.clear();
78
82
  this.#autoKeys = new WeakMap();
79
83
  cacheClear();
84
+ clearAutoLoadRulesCache();
80
85
  clearExplorerCache();
81
86
  }
82
87
  /**
@@ -265,8 +270,10 @@ export class ConfigProvider {
265
270
  */
266
271
  async search(targetFile) {
267
272
  const isExists = await targetFile.dirExists();
268
- cpLog('search: %s', targetFile.path);
269
- cpLog('isExists: %s', isExists);
273
+ if (cpLog.enabled) {
274
+ cpLog('search: %s', targetFile.path);
275
+ cpLog('isExists: %s', isExists);
276
+ }
270
277
  if (!isExists) {
271
278
  return null;
272
279
  }
package/lib/debug.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  import debug from 'debug';
2
2
  export type Log = debug.Debugger;
3
+ /**
4
+ * Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
5
+ * `enableDebug()`) can switch a namespace on after import.
6
+ */
3
7
  export declare const log: debug.Debugger;
package/lib/debug.js CHANGED
@@ -1,2 +1,6 @@
1
1
  import debug from 'debug';
2
+ /**
3
+ * Read `.enabled` at call time; never cache it at module load. `debug.enable()` (including via `verbosely()` /
4
+ * `enableDebug()`) can switch a namespace on after import.
5
+ */
2
6
  export const log = debug('ml-fr');
@@ -101,31 +101,30 @@ export declare function createPretenderResolver(config: PretendersConfig): Prete
101
101
  */
102
102
  export declare function resolvePretenders(config: PretendersConfig, context?: ResolvePretendersContext, options?: ResolvePretendersOptions): Promise<Pretender[]>;
103
103
  /**
104
- * Resolves selector collisions in `pretenders` for the specific file about
105
- * to be linted, deferring to `@markuplint/pretenders`' `disambiguatePretenders`
106
- * only when there's actually a same-selector, file-backed collision to
107
- * resolve — this keeps the common case (no ambiguity) free of both the
108
- * dynamic import and any file/AST work.
104
+ * Resolves, for the specific file about to be linted, which of the
105
+ * file-backed `pretenders` it actually refers to — a selector shared by
106
+ * several of them, and a name it imports from a package (a lone entry
107
+ * included, as it may be a package's component that shares the name) — by
108
+ * deferring to `@markuplint/pretenders`' `disambiguatePretenders`. Without a
109
+ * file-backed entry there is nothing to resolve, which keeps that common case
110
+ * free of the dynamic import; `disambiguatePretenders` itself skips the
111
+ * file/AST work for a source that names none of them.
109
112
  *
110
113
  * @param filePath - Absolute path of the file being linted
111
114
  * @param sourceCode - Full source text of the file being linted
112
115
  * @param pretenders - The flat pretender list {@link resolvePretenders} produced
113
116
  * @param options - Where to report the files the result depends on
114
117
  * @returns The disambiguated pretender list, or `pretenders` itself (same
115
- * reference) when there was no collision to resolve
118
+ * reference) when there was nothing to resolve
116
119
  */
117
120
  export declare function disambiguatePretendersForFile(filePath: string, sourceCode: string, pretenders: readonly Pretender[], options?: ResolvePretendersOptions): Promise<readonly Pretender[]>;
118
121
  /**
119
- * Deliberately gates on selector+filePath duplication alone — NOT on the
120
- * selector name shape `@markuplint/pretenders`' `disambiguatePretenders`
121
- * actually resolves (plain identifiers only). Duplicating that name-shape
122
- * check here would let the two independently maintained filters drift out
123
- * of sync: if the real filter is ever loosened without updating this one,
124
- * this fast-path gate would keep skipping the dynamic import for cases the
125
- * real logic would now handle, silently disabling disambiguation for them.
126
- * Being a strict superset costs at most an unnecessary dynamic import for
127
- * selectors the real logic ends up not touching — never a missed one.
122
+ * Whether two or more `filePath`-backed entries share a selector, whatever the
123
+ * selector name shape.
128
124
  *
125
+ * @deprecated Nothing in markuplint calls it any more, and it has no replacement:
126
+ * {@link disambiguatePretendersForFile} decides by itself whether there is anything
127
+ * to resolve. To be removed in the next major version.
129
128
  * @param pretenders - The flat pretender list to check
130
129
  * @returns `true` if some `selector` is shared by two or more `filePath`-backed entries
131
130
  */
@@ -164,37 +164,42 @@ async function appendAutoPretenders(data, context, auto, dependencies) {
164
164
  }
165
165
  }
166
166
  /**
167
- * Resolves selector collisions in `pretenders` for the specific file about
168
- * to be linted, deferring to `@markuplint/pretenders`' `disambiguatePretenders`
169
- * only when there's actually a same-selector, file-backed collision to
170
- * resolve — this keeps the common case (no ambiguity) free of both the
171
- * dynamic import and any file/AST work.
167
+ * Resolves, for the specific file about to be linted, which of the
168
+ * file-backed `pretenders` it actually refers to — a selector shared by
169
+ * several of them, and a name it imports from a package (a lone entry
170
+ * included, as it may be a package's component that shares the name) — by
171
+ * deferring to `@markuplint/pretenders`' `disambiguatePretenders`. Without a
172
+ * file-backed entry there is nothing to resolve, which keeps that common case
173
+ * free of the dynamic import; `disambiguatePretenders` itself skips the
174
+ * file/AST work for a source that names none of them.
172
175
  *
173
176
  * @param filePath - Absolute path of the file being linted
174
177
  * @param sourceCode - Full source text of the file being linted
175
178
  * @param pretenders - The flat pretender list {@link resolvePretenders} produced
176
179
  * @param options - Where to report the files the result depends on
177
180
  * @returns The disambiguated pretender list, or `pretenders` itself (same
178
- * reference) when there was no collision to resolve
181
+ * reference) when there was nothing to resolve
179
182
  */
180
183
  export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders, options) {
181
- if (!hasResolvableCollision(pretenders)) {
184
+ if (!pretenders.some(pretender => pretender.filePath)) {
182
185
  return pretenders;
183
186
  }
184
187
  const { disambiguatePretenders } = await import('@markuplint/pretenders');
185
- return disambiguatePretenders(pretenders, { filePath, sourceCode, dependencies: options?.dependencies });
188
+ // The same base the scanned pretenders' paths were made absolute from (see `rebasePretenderFilePaths`).
189
+ return disambiguatePretenders(pretenders, {
190
+ filePath,
191
+ sourceCode,
192
+ dependencies: options?.dependencies,
193
+ cwd: process.cwd(),
194
+ });
186
195
  }
187
196
  /**
188
- * Deliberately gates on selector+filePath duplication alone — NOT on the
189
- * selector name shape `@markuplint/pretenders`' `disambiguatePretenders`
190
- * actually resolves (plain identifiers only). Duplicating that name-shape
191
- * check here would let the two independently maintained filters drift out
192
- * of sync: if the real filter is ever loosened without updating this one,
193
- * this fast-path gate would keep skipping the dynamic import for cases the
194
- * real logic would now handle, silently disabling disambiguation for them.
195
- * Being a strict superset costs at most an unnecessary dynamic import for
196
- * selectors the real logic ends up not touching — never a missed one.
197
+ * Whether two or more `filePath`-backed entries share a selector, whatever the
198
+ * selector name shape.
197
199
  *
200
+ * @deprecated Nothing in markuplint calls it any more, and it has no replacement:
201
+ * {@link disambiguatePretendersForFile} decides by itself whether there is anything
202
+ * to resolve. To be removed in the next major version.
198
203
  * @param pretenders - The flat pretender list to check
199
204
  * @returns `true` if some `selector` is shared by two or more `filePath`-backed entries
200
205
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/file-resolver",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "The file resolver of markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,8 @@
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
18
- "import": "./lib/index.js",
19
- "types": "./lib/index.d.ts"
18
+ "types": "./lib/index.d.ts",
19
+ "default": "./lib/index.js"
20
20
  }
21
21
  },
22
22
  "publishConfig": {
@@ -31,25 +31,25 @@
31
31
  "clean": "tsc --build --clean"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "24.19.1"
34
+ "@types/node": "24.19.2"
35
35
  },
36
36
  "dependencies": {
37
- "@markuplint/html-parser": "5.2.0",
38
- "@markuplint/ml-ast": "5.1.1",
39
- "@markuplint/ml-config": "5.2.0",
40
- "@markuplint/ml-core": "5.2.0",
41
- "@markuplint/ml-spec": "5.2.0",
42
- "@markuplint/parser-utils": "5.2.0",
43
- "@markuplint/pretenders": "5.2.0",
44
- "@markuplint/selector": "5.2.0",
45
- "@markuplint/shared": "5.2.0",
46
- "cosmiconfig": "^9.0.2",
37
+ "@markuplint/html-parser": "5.3.0",
38
+ "@markuplint/ml-ast": "5.3.0",
39
+ "@markuplint/ml-config": "5.3.0",
40
+ "@markuplint/ml-core": "5.3.0",
41
+ "@markuplint/ml-spec": "5.3.0",
42
+ "@markuplint/parser-utils": "5.3.0",
43
+ "@markuplint/pretenders": "5.3.0",
44
+ "@markuplint/selector": "5.3.0",
45
+ "@markuplint/shared": "5.3.0",
46
+ "cosmiconfig": "^10.0.1",
47
47
  "debug": "^4.4.3",
48
48
  "glob": "^13.0.6",
49
49
  "ignore": "^7.0.12",
50
50
  "import-meta-resolve": "^4.2.0",
51
- "jsonc": "^2.0.0",
51
+ "jsonc": "^3.0.0",
52
52
  "minimatch": "^10.2.6"
53
53
  },
54
- "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
54
+ "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
55
55
  }