@markuplint/file-resolver 5.0.0-rc.4 → 5.0.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
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.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **file-resolver:** make generalImport() OS-independent for POSIX absolute paths ([23aa492](https://github.com/markuplint/markuplint/commit/23aa492202a4b305022651210d9fc413e9baef2d)), closes [#3841](https://github.com/markuplint/markuplint/issues/3841) [#3843](https://github.com/markuplint/markuplint/issues/3843) [#3840](https://github.com/markuplint/markuplint/issues/3840)
11
+ - **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)
12
+
13
+ ### Code Refactoring
14
+
15
+ - **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)
16
+
17
+ ### Features
18
+
19
+ - 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)
20
+
21
+ ### BREAKING CHANGES
22
+
23
+ - **rules:** with no alias coverage.
24
+
6
25
  # [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
26
 
8
27
  **Note:** Version bump only for package @markuplint/file-resolver
@@ -1,11 +1,10 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
4
  import { isFatalError } from '@markuplint/shared';
6
5
  import { resolve } from 'import-meta-resolve';
7
6
  import { log } from './debug.js';
8
- import { fromFileURL } from './path-utils.js';
7
+ import { fromFileURL, toFileURL } from './path-utils.js';
9
8
  const gLog = log.extend('general-import');
10
9
  const gLogSuccess = gLog.extend('success');
11
10
  const gLogError = gLog.extend('error');
@@ -27,11 +26,11 @@ export async function generalImport(name) {
27
26
  return result;
28
27
  }
29
28
  try {
30
- // Convert absolute paths to file:// URL format
31
- let importPath = name;
32
- if (path.isAbsolute(name)) {
33
- // Use Node.js pathToFileURL function to convert to a proper URL
34
- importPath = pathToFileURL(name).href;
29
+ // Convert absolute paths to file:// URL format. `toFileURL()` is
30
+ // OS-independent (see #3840 — Node's `pathToFileURL()` resolves a
31
+ // POSIX-style absolute path against the current Windows drive).
32
+ const importPath = toFileURL(name);
33
+ if (importPath !== name) {
35
34
  gLog('Converted to file URL: %s', importPath);
36
35
  }
37
36
  const imported = await import(importPath);
@@ -41,6 +40,18 @@ export async function generalImport(name) {
41
40
  return mod;
42
41
  }
43
42
  catch (error) {
43
+ // NOTE: `isFatalError()` is intentionally NOT applied at this catch
44
+ // boundary. `await import()` / `require()` invoke third-party module
45
+ // code, so any Tier-1-shaped error (TypeError / SyntaxError / etc.)
46
+ // at this point may originate from inside the imported module and
47
+ // not from markuplint's own code — we cannot distinguish the two.
48
+ // The Tier 1 classification (see `isFatalError()` in
49
+ // `@markuplint/shared`) only covers errors raised by markuplint's
50
+ // own code, which excludes third-party import failures (e.g. Node 22+ removing
51
+ // import assertion syntax, or bun's stricter ESM parser). Treating
52
+ // every error as a recoverable null-return is the correct policy
53
+ // here; callers (config / parser / plugin loaders) decide how to
54
+ // surface the missing module.
44
55
  if (
45
56
  // @ts-ignore
46
57
  'code' in error &&
@@ -93,13 +104,10 @@ export async function generalImport(name) {
93
104
  }
94
105
  }
95
106
  /**
96
- * Detects a bare package subpath specifier (e.g., `@scope/pkg/file.json`)
97
- * and resolves it to an absolute file path when the package's exports map
98
- * does not include the subpath.
99
- *
100
- * Returns the absolute path if resolution succeeds, or `null` if:
107
+ * Returns `null` (rather than throwing) in three distinct cases that callers
108
+ * treat identically no bypass is needed:
101
109
  * - The specifier is not a package subpath (absolute path, relative path, no subpath)
102
- * - The package's exports map already includes the subpath (no bypass needed)
110
+ * - The package's exports map already includes the subpath
103
111
  * - The package cannot be found at all
104
112
  */
105
113
  function resolvePackageSubpath(name) {
@@ -138,9 +146,6 @@ function resolvePackageSubpath(name) {
138
146
  return null;
139
147
  }
140
148
  }
141
- /**
142
- * Resolves the root directory of a package by name.
143
- */
144
149
  function resolvePackageDir(packageName) {
145
150
  // Try CJS require.resolve first — it can often resolve package.json
146
151
  // even when the ESM exports map doesn't include it
@@ -1,19 +1,33 @@
1
- /**
2
- * Convert OS-native separators to forward slashes.
3
- * Identity function on POSIX.
4
- */
1
+ /** Identity function on POSIX. */
5
2
  export declare function toSlash(filePath: string): string;
6
- /**
7
- * Convert a `file://` URL to a native file path using Node.js built-in.
8
- * Correctly handles URL encoding, UNC paths, and all drive letters.
9
- */
10
3
  export declare function fromFileURL(fileUrl: string): string;
11
4
  /**
12
- * Normalize a path for the `ignore` library (gitignore-style matching).
13
- * Removes drive letters, converts to forward slashes, and optionally makes relative.
5
+ * Convert an absolute file path (Windows or POSIX) into a `file://` URL
6
+ * suitable for `import()`. Bare module specifiers and relative paths are
7
+ * returned unchanged.
8
+ *
9
+ * Node's `pathToFileURL()` is intentionally avoided: on Windows it
10
+ * resolves a POSIX-style absolute path against the *current drive* and
11
+ * emits `file:///D:/tmp/foo` instead of `file:///tmp/foo` (#3840); on
12
+ * POSIX it likewise mishandles Windows-style drive paths. Constructing
13
+ * the URL ourselves keeps the function OS-independent so POSIX CI
14
+ * exercises the Windows code path. Each segment is percent-encoded so
15
+ * that spaces (`Program Files`), non-ASCII characters (e.g. Japanese
16
+ * usernames), and URL-reserved characters like `#` / `?` do not get
17
+ * reinterpreted as fragment / query delimiters by Node's URL parser.
18
+ *
19
+ * Mirrors `vscode/src/server/get-module.ts`'s `toImportSpecifier()` —
20
+ * keep the two in sync when adjusting Windows-path handling.
21
+ *
22
+ * Known limitation: UNC paths (`\\server\share\...`) are passed through
23
+ * unchanged.
24
+ *
25
+ * @see https://github.com/markuplint/markuplint/issues/3840
26
+ * @see https://github.com/markuplint/markuplint/issues/3836
27
+ * @see https://nodejs.org/api/esm.html#urls
14
28
  */
29
+ export declare function toFileURL(filePath: string): string;
30
+ /** Normalizes for the `ignore` library, whose matching is gitignore-style. */
15
31
  export declare function normalizeForIgnore(filePath: string, relative?: boolean): string;
16
- /**
17
- * Normalize a path for glob libraries (forward slashes required).
18
- */
32
+ /** Glob libraries require forward slashes. */
19
33
  export declare function normalizeForGlob(filePath: string): string;
package/lib/path-utils.js CHANGED
@@ -1,23 +1,57 @@
1
1
  import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
- /**
4
- * Convert OS-native separators to forward slashes.
5
- * Identity function on POSIX.
6
- */
3
+ /** Identity function on POSIX. */
7
4
  export function toSlash(filePath) {
8
5
  return filePath.replaceAll('\\', '/');
9
6
  }
10
- /**
11
- * Convert a `file://` URL to a native file path using Node.js built-in.
12
- * Correctly handles URL encoding, UNC paths, and all drive letters.
13
- */
14
7
  export function fromFileURL(fileUrl) {
15
8
  return fileURLToPath(fileUrl);
16
9
  }
17
10
  /**
18
- * Normalize a path for the `ignore` library (gitignore-style matching).
19
- * Removes drive letters, converts to forward slashes, and optionally makes relative.
11
+ * Convert an absolute file path (Windows or POSIX) into a `file://` URL
12
+ * suitable for `import()`. Bare module specifiers and relative paths are
13
+ * returned unchanged.
14
+ *
15
+ * Node's `pathToFileURL()` is intentionally avoided: on Windows it
16
+ * resolves a POSIX-style absolute path against the *current drive* and
17
+ * emits `file:///D:/tmp/foo` instead of `file:///tmp/foo` (#3840); on
18
+ * POSIX it likewise mishandles Windows-style drive paths. Constructing
19
+ * the URL ourselves keeps the function OS-independent so POSIX CI
20
+ * exercises the Windows code path. Each segment is percent-encoded so
21
+ * that spaces (`Program Files`), non-ASCII characters (e.g. Japanese
22
+ * usernames), and URL-reserved characters like `#` / `?` do not get
23
+ * reinterpreted as fragment / query delimiters by Node's URL parser.
24
+ *
25
+ * Mirrors `vscode/src/server/get-module.ts`'s `toImportSpecifier()` —
26
+ * keep the two in sync when adjusting Windows-path handling.
27
+ *
28
+ * Known limitation: UNC paths (`\\server\share\...`) are passed through
29
+ * unchanged.
30
+ *
31
+ * @see https://github.com/markuplint/markuplint/issues/3840
32
+ * @see https://github.com/markuplint/markuplint/issues/3836
33
+ * @see https://nodejs.org/api/esm.html#urls
20
34
  */
35
+ export function toFileURL(filePath) {
36
+ const isWindowsAbsolute = /^[a-z]:[/\\]/i.test(filePath);
37
+ const isPosixAbsolute = filePath.startsWith('/');
38
+ if (!isWindowsAbsolute && !isPosixAbsolute) {
39
+ return filePath;
40
+ }
41
+ if (isWindowsAbsolute) {
42
+ // The drive-letter segment (`c:`) is kept as-is to match the
43
+ // `pathToFileURL` output shape on Windows (`file:///c:/...`).
44
+ const [drive, ...rest] = filePath.replaceAll('\\', '/').split('/');
45
+ const encoded = [drive, ...rest.map(segment => encodeURIComponent(segment))].join('/');
46
+ return `file:///${encoded}`;
47
+ }
48
+ // POSIX absolute path. Splitting `/tmp/foo` by `/` yields `['', 'tmp',
49
+ // 'foo']`; the leading empty element produces the `file:///` prefix
50
+ // after `join('/')`, so the round-trip is exactly `file:///tmp/foo`.
51
+ const segments = filePath.split('/').map(segment => encodeURIComponent(segment));
52
+ return `file://${segments.join('/')}`;
53
+ }
54
+ /** Normalizes for the `ignore` library, whose matching is gitignore-style. */
21
55
  export function normalizeForIgnore(filePath, relative = false) {
22
56
  const hasBang = filePath.startsWith('!');
23
57
  if (hasBang) {
@@ -38,9 +72,7 @@ export function normalizeForIgnore(filePath, relative = false) {
38
72
  }
39
73
  return filePath;
40
74
  }
41
- /**
42
- * Normalize a path for glob libraries (forward slashes required).
43
- */
75
+ /** Glob libraries require forward slashes. */
44
76
  export function normalizeForGlob(filePath) {
45
77
  return toSlash(filePath);
46
78
  }
@@ -1,8 +1,20 @@
1
1
  import type { OptimizedConfig, Pretender } from '@markuplint/ml-config';
2
2
  type PretendersConfig = OptimizedConfig['pretenders'];
3
+ /**
4
+ * The lint target's own identity, needed only to resolve `config.auto`.
5
+ * Omitting this (or omitting `config.auto`) skips auto-resolution entirely,
6
+ * so every other resolution source works exactly as before without it.
7
+ */
8
+ export type ResolvePretendersContext = {
9
+ /** Absolute path of the file being linted */
10
+ readonly filePath: string;
11
+ /** Full source text of the file being linted (may be unsaved editor content) */
12
+ readonly sourceCode: string;
13
+ };
3
14
  /**
4
15
  * Resolves pretender definitions from files, imported modules, inline data,
5
- * and dynamic component scanning in the configuration.
16
+ * dynamic component scanning, and (when `context` is given) the lint
17
+ * target's own import graph.
6
18
  *
7
19
  * Resolution order:
8
20
  * 1. `config.files` — direct import of pretender data files
@@ -12,9 +24,53 @@ type PretendersConfig = OptimizedConfig['pretenders'];
12
24
  * 3. `config.data` — inline pretender definitions
13
25
  * 4. `config.scan` — dynamic component scanning via glob patterns
14
26
  * (`files` accepts `string | string[]`)
27
+ * 5. `config.auto` — on-demand scan of `context`'s own import graph (requires
28
+ * `context`; a no-op without it, e.g. when the caller has no lint target yet)
15
29
  *
16
30
  * @param config - The pretenders configuration section from the optimized config
31
+ * @param context - The lint target's path/source, required only for `config.auto`
17
32
  * @returns An array of all resolved pretender definitions
18
33
  */
19
- export declare function resolvePretenders(config: PretendersConfig): Promise<Pretender[]>;
34
+ export declare function resolvePretenders(config: PretendersConfig, context?: ResolvePretendersContext): Promise<Pretender[]>;
35
+ /**
36
+ * Resolves selector collisions in `pretenders` for the specific file about
37
+ * to be linted, deferring to `@markuplint/pretenders`' `disambiguatePretenders`
38
+ * only when there's actually a same-selector, file-backed collision to
39
+ * resolve — this keeps the common case (no ambiguity) free of both the
40
+ * dynamic import and any file/AST work.
41
+ *
42
+ * @param filePath - Absolute path of the file being linted
43
+ * @param sourceCode - Full source text of the file being linted
44
+ * @param pretenders - The flat pretender list {@link resolvePretenders} produced
45
+ * @returns The disambiguated pretender list, or `pretenders` itself (same
46
+ * reference) when there was no collision to resolve
47
+ */
48
+ export declare function disambiguatePretendersForFile(filePath: string, sourceCode: string, pretenders: readonly Pretender[]): Promise<readonly Pretender[]>;
49
+ /**
50
+ * Deliberately gates on selector+filePath duplication alone — NOT on the
51
+ * selector name shape `@markuplint/pretenders`' `disambiguatePretenders`
52
+ * actually resolves (plain identifiers only). Duplicating that name-shape
53
+ * check here would let the two independently maintained filters drift out
54
+ * of sync: if the real filter is ever loosened without updating this one,
55
+ * this fast-path gate would keep skipping the dynamic import for cases the
56
+ * real logic would now handle, silently disabling disambiguation for them.
57
+ * Being a strict superset costs at most an unnecessary dynamic import for
58
+ * selectors the real logic ends up not touching — never a missed one.
59
+ *
60
+ * @param pretenders - The flat pretender list to check
61
+ * @returns `true` if some `selector` is shared by two or more `filePath`-backed entries
62
+ */
63
+ export declare function hasResolvableCollision(pretenders: readonly Pretender[]): boolean;
64
+ /**
65
+ * Clears `@markuplint/pretenders`' module-level import/export resolution
66
+ * caches. Call this whenever a lint host re-resolves config without cache
67
+ * (e.g. watch mode after a file change) — otherwise a renamed export or a
68
+ * newly valid tsconfig `paths` alias keeps resolving as it did before the
69
+ * change for the rest of the process's lifetime. A no-op (not an error) when
70
+ * `@markuplint/pretenders` isn't installed, since nothing has populated its
71
+ * caches in that case either.
72
+ *
73
+ * @returns A promise that resolves once the caches have been cleared
74
+ */
75
+ export declare function invalidatePretenderResolutionCaches(): Promise<void>;
20
76
  export {};
@@ -1,9 +1,22 @@
1
1
  import path from 'node:path';
2
+ import { rebasePretenderFilePath } from '@markuplint/ml-config';
2
3
  import { glob } from 'glob';
3
4
  import { generalImport } from './general-import.js';
5
+ /**
6
+ * `Pretender.filePath` is written by scanners relative to their own base
7
+ * directory, which is meaningless once entries from files/scan results
8
+ * scattered across different directories are merged into one flat list.
9
+ * Rebasing to an absolute path immediately after each source is read is
10
+ * what lets {@link disambiguatePretendersForFile} later compare a
11
+ * pretender's origin file against the lint target's resolved imports.
12
+ */
13
+ function rebasePretenderFilePaths(pretenders, baseDir) {
14
+ return pretenders.map(pretender => rebasePretenderFilePath(pretender, relPath => path.resolve(baseDir, relPath)));
15
+ }
4
16
  /**
5
17
  * Resolves pretender definitions from files, imported modules, inline data,
6
- * and dynamic component scanning in the configuration.
18
+ * dynamic component scanning, and (when `context` is given) the lint
19
+ * target's own import graph.
7
20
  *
8
21
  * Resolution order:
9
22
  * 1. `config.files` — direct import of pretender data files
@@ -13,11 +26,14 @@ import { generalImport } from './general-import.js';
13
26
  * 3. `config.data` — inline pretender definitions
14
27
  * 4. `config.scan` — dynamic component scanning via glob patterns
15
28
  * (`files` accepts `string | string[]`)
29
+ * 5. `config.auto` — on-demand scan of `context`'s own import graph (requires
30
+ * `context`; a no-op without it, e.g. when the caller has no lint target yet)
16
31
  *
17
32
  * @param config - The pretenders configuration section from the optimized config
33
+ * @param context - The lint target's path/source, required only for `config.auto`
18
34
  * @returns An array of all resolved pretender definitions
19
35
  */
20
- export async function resolvePretenders(config) {
36
+ export async function resolvePretenders(config, context) {
21
37
  if (!config) {
22
38
  return [];
23
39
  }
@@ -28,7 +44,9 @@ export async function resolvePretenders(config) {
28
44
  if (!pretenderFile?.data) {
29
45
  continue;
30
46
  }
31
- data.push(...pretenderFile.data);
47
+ // `file` is already absolute (resolved by the config provider), so its
48
+ // own directory is the correct base for the entries it carries.
49
+ data.push(...rebasePretenderFilePaths(pretenderFile.data, path.dirname(file)));
32
50
  }
33
51
  }
34
52
  if (config.imports) {
@@ -40,6 +58,10 @@ export async function resolvePretenders(config) {
40
58
  if (!pretenderFile?.data) {
41
59
  continue;
42
60
  }
61
+ // The on-disk location of an npm package's pretenders data isn't
62
+ // recoverable from `generalImport`'s return value, so these entries'
63
+ // filePath is left as-is — disambiguation simply can't confirm them
64
+ // (see the module JSDoc for the fallback policy this implies).
43
65
  data.push(...pretenderFile.data);
44
66
  }
45
67
  }
@@ -56,9 +78,100 @@ export async function resolvePretenders(config) {
56
78
  const scanned = await scan(resolved, {
57
79
  ignoreComponentNames: entry.ignoreComponentNames ? [...entry.ignoreComponentNames] : undefined,
58
80
  });
59
- data.push(...scanned);
81
+ // `scan()` (with no `cwd` option) reports filePath relative to `process.cwd()`.
82
+ data.push(...rebasePretenderFilePaths(scanned, process.cwd()));
83
+ }
84
+ }
85
+ }
86
+ if (config.auto && context) {
87
+ const { autoScan } = await import('@markuplint/pretenders');
88
+ const scanned = await autoScan(context.filePath, context.sourceCode);
89
+ // `autoScan()` reports filePath relative to `process.cwd()`, same as `scan()`.
90
+ const rebased = rebasePretenderFilePaths(scanned, process.cwd());
91
+ // `scan` and `auto` can both walk into the same file (e.g. a component
92
+ // `scan`'s glob already covers that `auto`'s import-graph walk also
93
+ // reaches); de-duping on (selector, filePath) keeps that file's entry
94
+ // from appearing twice while still letting a same-selector entry from a
95
+ // genuinely different file through for `disambiguatePretendersForFile`
96
+ // to resolve. `selector` is a markuplint CSS-like selector and can
97
+ // legitimately contain spaces (a descendant combinator), so the pair is
98
+ // joined via `JSON.stringify` rather than a plain-string delimiter —
99
+ // otherwise two distinct (selector, filePath) pairs could concatenate to
100
+ // the same string and be mistaken for a duplicate.
101
+ const dedupeKey = (p) => JSON.stringify([p.selector, p.filePath]);
102
+ const seen = new Set(data.filter(p => p.filePath).map(p => dedupeKey(p)));
103
+ for (const pretender of rebased) {
104
+ if (pretender.filePath) {
105
+ const key = dedupeKey(pretender);
106
+ if (seen.has(key)) {
107
+ continue;
108
+ }
109
+ seen.add(key);
60
110
  }
111
+ data.push(pretender);
61
112
  }
62
113
  }
63
114
  return data;
64
115
  }
116
+ /**
117
+ * Resolves selector collisions in `pretenders` for the specific file about
118
+ * to be linted, deferring to `@markuplint/pretenders`' `disambiguatePretenders`
119
+ * only when there's actually a same-selector, file-backed collision to
120
+ * resolve — this keeps the common case (no ambiguity) free of both the
121
+ * dynamic import and any file/AST work.
122
+ *
123
+ * @param filePath - Absolute path of the file being linted
124
+ * @param sourceCode - Full source text of the file being linted
125
+ * @param pretenders - The flat pretender list {@link resolvePretenders} produced
126
+ * @returns The disambiguated pretender list, or `pretenders` itself (same
127
+ * reference) when there was no collision to resolve
128
+ */
129
+ export async function disambiguatePretendersForFile(filePath, sourceCode, pretenders) {
130
+ if (!hasResolvableCollision(pretenders)) {
131
+ return pretenders;
132
+ }
133
+ const { disambiguatePretenders } = await import('@markuplint/pretenders');
134
+ return disambiguatePretenders(pretenders, { filePath, sourceCode });
135
+ }
136
+ /**
137
+ * Deliberately gates on selector+filePath duplication alone — NOT on the
138
+ * selector name shape `@markuplint/pretenders`' `disambiguatePretenders`
139
+ * actually resolves (plain identifiers only). Duplicating that name-shape
140
+ * check here would let the two independently maintained filters drift out
141
+ * of sync: if the real filter is ever loosened without updating this one,
142
+ * this fast-path gate would keep skipping the dynamic import for cases the
143
+ * real logic would now handle, silently disabling disambiguation for them.
144
+ * Being a strict superset costs at most an unnecessary dynamic import for
145
+ * selectors the real logic ends up not touching — never a missed one.
146
+ *
147
+ * @param pretenders - The flat pretender list to check
148
+ * @returns `true` if some `selector` is shared by two or more `filePath`-backed entries
149
+ */
150
+ export function hasResolvableCollision(pretenders) {
151
+ const seen = new Set();
152
+ for (const pretender of pretenders) {
153
+ if (!pretender.filePath) {
154
+ continue;
155
+ }
156
+ if (seen.has(pretender.selector)) {
157
+ return true;
158
+ }
159
+ seen.add(pretender.selector);
160
+ }
161
+ return false;
162
+ }
163
+ /**
164
+ * Clears `@markuplint/pretenders`' module-level import/export resolution
165
+ * caches. Call this whenever a lint host re-resolves config without cache
166
+ * (e.g. watch mode after a file change) — otherwise a renamed export or a
167
+ * newly valid tsconfig `paths` alias keeps resolving as it did before the
168
+ * change for the rest of the process's lifetime. A no-op (not an error) when
169
+ * `@markuplint/pretenders` isn't installed, since nothing has populated its
170
+ * caches in that case either.
171
+ *
172
+ * @returns A promise that resolves once the caches have been cleared
173
+ */
174
+ export async function invalidatePretenderResolutionCaches() {
175
+ const pretendersMod = await import('@markuplint/pretenders').catch(() => null);
176
+ pretendersMod?.clearPretenderCaches();
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/file-resolver",
3
- "version": "5.0.0-rc.4",
3
+ "version": "5.0.0-rc.5",
4
4
  "description": "The file resolver of markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,7 @@
10
10
  "author": "Yusuke Hirao <yusukehirao@me.com>",
11
11
  "license": "MIT",
12
12
  "engines": {
13
- "node": ">=22"
13
+ "node": ">=24"
14
14
  },
15
15
  "type": "module",
16
16
  "exports": {
@@ -31,18 +31,18 @@
31
31
  "clean": "tsc --build --clean"
32
32
  },
33
33
  "devDependencies": {
34
- "@types/node": "24.5.1"
34
+ "@types/node": "24.13.3"
35
35
  },
36
36
  "dependencies": {
37
- "@markuplint/html-parser": "5.0.0-rc.4",
38
- "@markuplint/ml-ast": "5.0.0-rc.4",
39
- "@markuplint/ml-config": "5.0.0-rc.4",
40
- "@markuplint/ml-core": "5.0.0-rc.4",
41
- "@markuplint/ml-spec": "5.0.0-rc.4",
42
- "@markuplint/parser-utils": "5.0.0-rc.4",
43
- "@markuplint/pretenders": "5.0.0-rc.4",
44
- "@markuplint/selector": "5.0.0-rc.4",
45
- "@markuplint/shared": "5.0.0-rc.4",
37
+ "@markuplint/html-parser": "5.0.0-rc.5",
38
+ "@markuplint/ml-ast": "5.0.0-rc.5",
39
+ "@markuplint/ml-config": "5.0.0-rc.5",
40
+ "@markuplint/ml-core": "5.0.0-rc.5",
41
+ "@markuplint/ml-spec": "5.0.0-rc.5",
42
+ "@markuplint/parser-utils": "5.0.0-rc.5",
43
+ "@markuplint/pretenders": "5.0.0-rc.5",
44
+ "@markuplint/selector": "5.0.0-rc.5",
45
+ "@markuplint/shared": "5.0.0-rc.5",
46
46
  "cosmiconfig": "9.0.1",
47
47
  "debug": "4.4.3",
48
48
  "glob": "13.0.6",
@@ -51,5 +51,5 @@
51
51
  "jsonc": "2.0.0",
52
52
  "minimatch": "10.2.5"
53
53
  },
54
- "gitHead": "97a6339bbae23f556de5d307b3ce2ef7cfd9402d"
54
+ "gitHead": "8d87463af2ff3f1b83fb28da20f1819362cf3555"
55
55
  }