@markuplint/pretenders 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 (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +83 -5
  3. package/lib/auto-scan.d.ts +36 -0
  4. package/lib/auto-scan.js +137 -0
  5. package/lib/cli.d.ts +0 -10
  6. package/lib/cli.js +1 -11
  7. package/lib/create-scanner.d.ts +1 -6
  8. package/lib/create-scanner.js +1 -6
  9. package/lib/dependency-mapper.d.ts +40 -13
  10. package/lib/dependency-mapper.js +222 -16
  11. package/lib/disambiguate.d.ts +37 -0
  12. package/lib/disambiguate.js +102 -0
  13. package/lib/export-table.d.ts +45 -0
  14. package/lib/export-table.js +106 -0
  15. package/lib/import-resolver/analyze-jsx-imports.d.ts +31 -0
  16. package/lib/import-resolver/analyze-jsx-imports.js +99 -0
  17. package/lib/import-resolver/extract-script-source.d.ts +6 -30
  18. package/lib/import-resolver/extract-script-source.js +6 -26
  19. package/lib/import-resolver/index.d.ts +1 -0
  20. package/lib/import-resolver/index.js +15 -18
  21. package/lib/import-resolver/parse-imports.d.ts +0 -5
  22. package/lib/import-resolver/parse-imports.js +1 -20
  23. package/lib/import-resolver/resolve-barrel.js +0 -7
  24. package/lib/import-resolver/resolve-module-file.d.ts +47 -0
  25. package/lib/import-resolver/resolve-module-file.js +138 -0
  26. package/lib/index.d.ts +39 -0
  27. package/lib/index.js +47 -0
  28. package/lib/input.d.ts +0 -7
  29. package/lib/input.js +0 -7
  30. package/lib/jsx/compiler-host.d.ts +29 -0
  31. package/lib/jsx/compiler-host.js +56 -0
  32. package/lib/jsx/create-identify.d.ts +3 -9
  33. package/lib/jsx/create-identify.js +3 -9
  34. package/lib/jsx/finder.d.ts +0 -8
  35. package/lib/jsx/finder.js +2 -16
  36. package/lib/jsx/get-attributes.d.ts +2 -7
  37. package/lib/jsx/get-attributes.js +2 -7
  38. package/lib/jsx/get-children.d.ts +0 -7
  39. package/lib/jsx/get-children.js +0 -10
  40. package/lib/jsx/index.js +20 -15
  41. package/lib/out.d.ts +6 -6
  42. package/lib/out.js +16 -7
  43. package/lib/pretender-director.d.ts +24 -24
  44. package/lib/pretender-director.js +22 -21
  45. package/lib/scan.d.ts +5 -0
  46. package/lib/scan.js +5 -2
  47. package/lib/scanner-loader.d.ts +0 -6
  48. package/lib/scanner-loader.js +0 -18
  49. package/lib/template/derive-name.d.ts +0 -6
  50. package/lib/template/derive-name.js +0 -6
  51. package/lib/template/detect-slots.d.ts +0 -6
  52. package/lib/template/detect-slots.js +0 -6
  53. package/lib/template/extract-attrs.d.ts +0 -5
  54. package/lib/template/extract-attrs.js +0 -5
  55. package/lib/template/extract-root.d.ts +1 -6
  56. package/lib/template/extract-root.js +1 -6
  57. package/lib/template/index.js +20 -8
  58. package/lib/template/parse-component.d.ts +0 -16
  59. package/lib/template/parse-component.js +0 -21
  60. package/lib/template/types.d.ts +0 -3
  61. package/package.json +11 -10
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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
+ **Note:** Version bump only for package @markuplint/pretenders
9
+
10
+ # [5.0.0-rc.5](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.4...v5.0.0-rc.5) (2026-08-28)
11
+
12
+ ### Bug Fixes
13
+
14
+ - **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)
15
+
16
+ ### Features
17
+
18
+ - 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)
19
+
6
20
  # [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
21
 
8
22
  **Note:** Version bump only for package @markuplint/pretenders
package/README.md CHANGED
@@ -49,6 +49,21 @@ Instead of the CLI, you can configure dynamic scanning directly in your markupli
49
49
  }
50
50
  ```
51
51
 
52
+ ### On-demand Scanning
53
+
54
+ `scan` pre-scans a configured set of files once. Alternatively, the `auto` field resolves pretenders per lint target by walking the linted file's own import graph — no `files`/`scan` configuration needed, and same-named components in unrelated files can't collide since only files the linted file actually imports (transitively) are ever considered:
55
+
56
+ ```jsonc
57
+ // .markuplintrc
58
+ {
59
+ "pretenders": {
60
+ "auto": true,
61
+ },
62
+ }
63
+ ```
64
+
65
+ Only the config file is filesystem-watched, so in watch mode or an editor session, results can go stale if an imported component file changes without the config changing too.
66
+
52
67
  ## How It Works
53
68
 
54
69
  ### JSX Scanner
@@ -121,20 +136,25 @@ Slot detection covers:
121
136
 
122
137
  ### Import Resolver
123
138
 
124
- The import resolver analyzes `<script>` / frontmatter / ESM blocks in component files and extracts import bindings. This links template component usage to source file locations, enabling cross-file dependency resolution.
125
-
126
- Script source extraction is delegated to each parser's component-scanner (Vue, Svelte, Astro), while MDX extraction is built-in. Supported script block types:
139
+ The import resolver analyzes a component file and extracts import bindings, linking component usage to source file locations for cross-file dependency resolution.
127
140
 
128
141
  - Vue `<script setup>` (via `@markuplint/vue-parser/component-scanner`)
129
142
  - Vue Options API `<script>` (fallback when no `<script setup>`; only imports registered in `components: { ... }` are returned)
130
143
  - Svelte `<script>` (via `@markuplint/svelte-parser/component-scanner`; prefers instance script over module script)
131
144
  - Astro frontmatter (via `@markuplint/astro-parser/component-scanner`)
132
145
  - MDX top-level ESM (built-in)
146
+ - JS/TS/JSX/TSX (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts`): analyzed directly via the TypeScript AST, so JSX syntax and non-standard TS constructs (which `es-module-lexer` cannot parse) are handled correctly
133
147
 
134
148
  Dynamic imports with string literal specifiers (`import('./path')`) are included in bindings with `type: 'dynamic'`. Template literal and variable specifiers are excluded.
135
149
 
136
150
  Barrel file re-exports can be resolved with `resolveBarrelExport`, which maps a named import from a directory with an index file back to its original source module (single-level only).
137
151
 
152
+ ### Lint-time Disambiguation
153
+
154
+ When the same component name is declared in more than one scanned file (e.g., two unrelated `Item` components rendering different elements), the generated pretenders keep independent entries for each — but `markuplint`'s lint pipeline still needs to know, for the specific file being linted, which one it actually refers to. `disambiguatePretenders` resolves this from the lint target's own declarations and imports; it's normally invoked automatically by `markuplint`'s config resolution, not called directly.
155
+
156
+ In long-running hosts (watch mode, editor extensions) that keep re-resolving pretenders across file edits, call `clearPretenderCaches()` after each edit — otherwise a renamed export or a newly valid tsconfig `paths` alias keeps resolving as it did before the change for the rest of the process's lifetime.
157
+
138
158
  ## API
139
159
 
140
160
  ### `scan(files, options)`
@@ -210,9 +230,9 @@ const pretenders = await templateScanner(
210
230
 
211
231
  ### `analyzeImports(filePath, source)`
212
232
 
213
- Extracts import bindings from a component file's script block. Detects the framework from the file extension and extracts the appropriate source block automatically.
233
+ Extracts import bindings from a component file. Detects the framework from the file extension and extracts the appropriate source automatically — `.vue`, `.svelte`, `.astro`, and `.mdx` extract a script/frontmatter/ESM block first, while `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, and `.cts` are analyzed directly via the TypeScript AST (which, unlike `es-module-lexer`, can parse JSX syntax).
214
234
 
215
- Returns `null` if the file extension is not a supported framework (`.vue`, `.svelte`, `.astro`, `.mdx`).
235
+ Returns `null` if the file extension is not one of the above.
216
236
 
217
237
  ```ts
218
238
  import { analyzeImports } from '@markuplint/pretenders';
@@ -281,3 +301,61 @@ if (binding) {
281
301
  #### Returns
282
302
 
283
303
  `string | null` — The relative source path from the barrel file, or `null` if not a barrel or name not found.
304
+
305
+ ### `disambiguatePretenders(pretenders, options)`
306
+
307
+ Given a flat pretender list and the file currently being linted, resolves which of several same-selector candidates that file actually refers to. Entries without a `filePath`, or whose `selector` isn't a plain identifier, are never touched. Normally invoked automatically as part of `markuplint`'s config resolution.
308
+
309
+ ```ts
310
+ import { disambiguatePretenders } from '@markuplint/pretenders';
311
+
312
+ const resolved = await disambiguatePretenders(pretenders, {
313
+ filePath: '/absolute/path/to/Page.tsx',
314
+ sourceCode: source,
315
+ });
316
+ ```
317
+
318
+ #### Parameters
319
+
320
+ | Parameter | Type | Description |
321
+ | -------------------- | ---------------------- | ------------------------------------------- |
322
+ | `pretenders` | `readonly Pretender[]` | The resolved pretender list to disambiguate |
323
+ | `options.filePath` | `string` | Absolute path of the file being linted |
324
+ | `options.sourceCode` | `string` | Full source text of the file being linted |
325
+
326
+ #### Returns
327
+
328
+ `Promise<readonly Pretender[]>` — The disambiguated pretender list, or `pretenders` itself when there was nothing to resolve or nothing could be confirmed.
329
+
330
+ ### `autoScan(entryAbsPath, sourceCode)`
331
+
332
+ Resolves pretenders on demand by walking a single lint target's own import graph (breadth-first, extension-agnostic — a `.tsx` entry can import a `.vue` file and vice versa) and scanning the collected files in one batch. This is the resolution logic behind the `pretenders.auto` config option; it's normally invoked automatically as part of `markuplint`'s config resolution, not called directly.
333
+
334
+ ```ts
335
+ import { autoScan } from '@markuplint/pretenders';
336
+
337
+ const pretenders = await autoScan('/absolute/path/to/Page.tsx', sourceCode);
338
+ ```
339
+
340
+ #### Parameters
341
+
342
+ | Parameter | Type | Description |
343
+ | -------------- | -------- | ------------------------------------------------------------- |
344
+ | `entryAbsPath` | `string` | Absolute path of the file being linted |
345
+ | `sourceCode` | `string` | The entry file's current text (may be unsaved editor content) |
346
+
347
+ #### Returns
348
+
349
+ `Promise<Pretender[]>` — Discovered pretender mappings for the entry file and its import graph.
350
+
351
+ Results are cached per entry path, keyed on `sourceCode` equality (not mtime, which doesn't exist for unsaved editor content); `node_modules` is never traversed into, and import cycles are handled via a visited set. Traversal is capped at 8 import hops for template-language (`.vue`/`.svelte`/`.astro`) chains, but this cap does not hold for JSX/TSX-only chains: `jsxScanner` builds a `ts.Program` from the collected files, and TypeScript's own module resolution transitively pulls in whatever those files import regardless of the cap.
352
+
353
+ ### `clearPretenderCaches()`
354
+
355
+ Clears the module-level caches that back import/export resolution (module resolution, export tables, parsed JSX source files, `autoScan` results). None of these caches expire on their own, so a long-running host (watch mode, an editor extension) that keeps resolving pretenders across file edits must call this after each edit — otherwise a renamed export or a newly valid tsconfig `paths` alias keeps resolving as it did before the change for the rest of the process's lifetime.
356
+
357
+ ```ts
358
+ import { clearPretenderCaches } from '@markuplint/pretenders';
359
+
360
+ clearPretenderCaches();
361
+ ```
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @module auto-scan
3
+ *
4
+ * On-demand pretender resolution: given a single lint target's absolute path
5
+ * and current source text, walks its import graph (breadth-first) to collect
6
+ * the transitively-referenced component files, then scans all of them in one
7
+ * batch. This lets `pretenders: { auto: true }` work without any
8
+ * pre-configured `files`/`scan` glob, at the cost of running a scan per lint
9
+ * target instead of once per project (see `jsx/compiler-host.ts` for the
10
+ * content-based caching that offsets this).
11
+ *
12
+ * Traversal is extension-agnostic — a `.tsx` entry can import a `.vue` file
13
+ * and vice versa, even though TypeScript's own module resolution can't
14
+ * follow into `.vue` (it falls back to a filesystem search; see
15
+ * `resolveModuleFile`). Per-extension scanner dispatch only happens at the
16
+ * final `scan()` call.
17
+ */
18
+ import type { Pretender } from '@markuplint/ml-config';
19
+ /**
20
+ * Clears the module-level auto-scan result cache. This cache is keyed on the
21
+ * ENTRY file's path and content only, so an edit to any other file in the
22
+ * entry's import graph does not invalidate it — a long-running host must call
23
+ * `clearPretenderCaches()` (which includes this one) after an edit to any file
24
+ * `autoScan` may have walked, not just after an edit to the entry itself.
25
+ */
26
+ export declare function clearAutoScanCache(): void;
27
+ /**
28
+ * Resolves pretenders on demand by scanning `entryAbsPath`'s own import graph:
29
+ * the entry file plus every file it transitively imports (up to a fixed
30
+ * traversal depth), scanned together in one `scan()` call.
31
+ *
32
+ * @param entryAbsPath - Absolute path of the file currently being linted
33
+ * @param sourceCode - The entry file's current text (may be unsaved editor content)
34
+ * @returns Discovered pretender mappings for the entry file and its import graph
35
+ */
36
+ export declare function autoScan(entryAbsPath: string, sourceCode: string): Promise<Pretender[]>;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @module auto-scan
3
+ *
4
+ * On-demand pretender resolution: given a single lint target's absolute path
5
+ * and current source text, walks its import graph (breadth-first) to collect
6
+ * the transitively-referenced component files, then scans all of them in one
7
+ * batch. This lets `pretenders: { auto: true }` work without any
8
+ * pre-configured `files`/`scan` glob, at the cost of running a scan per lint
9
+ * target instead of once per project (see `jsx/compiler-host.ts` for the
10
+ * content-based caching that offsets this).
11
+ *
12
+ * Traversal is extension-agnostic — a `.tsx` entry can import a `.vue` file
13
+ * and vice versa, even though TypeScript's own module resolution can't
14
+ * follow into `.vue` (it falls back to a filesystem search; see
15
+ * `resolveModuleFile`). Per-extension scanner dispatch only happens at the
16
+ * final `scan()` call.
17
+ */
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { isFatalError } from '@markuplint/shared';
21
+ import { analyzeImports } from './import-resolver/index.js';
22
+ import { normalizePath, resolveModuleFile } from './import-resolver/resolve-module-file.js';
23
+ import { scan } from './scan.js';
24
+ const SCANNABLE_EXTENSIONS = new Set([
25
+ '.js',
26
+ '.jsx',
27
+ '.ts',
28
+ '.tsx',
29
+ '.mjs',
30
+ '.cjs',
31
+ '.mts',
32
+ '.cts',
33
+ '.vue',
34
+ '.svelte',
35
+ '.astro',
36
+ ]);
37
+ // Bounds runaway traversal (a deep or wrongly resolved chain) rather than
38
+ // expressing a real limit on legitimate component nesting. Note this only
39
+ // bounds which files BFS explicitly visits: for JSX/TSX files, jsxScanner
40
+ // builds a ts.Program from the collected file list, and TypeScript's own
41
+ // module resolution transitively pulls in whatever those files import —
42
+ // including files past this depth — so the cap is not a hard ceiling on
43
+ // what ends up scanned when the chain is JSX/TSX throughout.
44
+ const MAX_DEPTH = 8;
45
+ const resultCache = new Map();
46
+ /**
47
+ * Clears the module-level auto-scan result cache. This cache is keyed on the
48
+ * ENTRY file's path and content only, so an edit to any other file in the
49
+ * entry's import graph does not invalidate it — a long-running host must call
50
+ * `clearPretenderCaches()` (which includes this one) after an edit to any file
51
+ * `autoScan` may have walked, not just after an edit to the entry itself.
52
+ */
53
+ export function clearAutoScanCache() {
54
+ resultCache.clear();
55
+ }
56
+ /**
57
+ * Resolves pretenders on demand by scanning `entryAbsPath`'s own import graph:
58
+ * the entry file plus every file it transitively imports (up to a fixed
59
+ * traversal depth), scanned together in one `scan()` call.
60
+ *
61
+ * @param entryAbsPath - Absolute path of the file currently being linted
62
+ * @param sourceCode - The entry file's current text (may be unsaved editor content)
63
+ * @returns Discovered pretender mappings for the entry file and its import graph
64
+ */
65
+ export async function autoScan(entryAbsPath, sourceCode) {
66
+ const entryKey = normalizePath(entryAbsPath);
67
+ const cached = resultCache.get(entryKey);
68
+ if (cached && cached.sourceCode === sourceCode) {
69
+ return cached.pretenders;
70
+ }
71
+ const sources = new Map([[entryKey, sourceCode]]);
72
+ const visited = new Set([entryKey]);
73
+ const collected = [];
74
+ if (isScannable(entryAbsPath)) {
75
+ collected.push(entryAbsPath);
76
+ }
77
+ let frontier = [
78
+ { absPath: entryAbsPath, source: sourceCode },
79
+ ];
80
+ for (let depth = 0; depth < MAX_DEPTH && frontier.length > 0; depth++) {
81
+ const nextFrontier = [];
82
+ for (const { absPath, source } of frontier) {
83
+ const analysis = await analyzeImports(absPath, source);
84
+ if (!analysis) {
85
+ continue;
86
+ }
87
+ for (const binding of analysis.bindings) {
88
+ const resolved = resolveModuleFile(absPath, binding.source);
89
+ if (!resolved) {
90
+ continue;
91
+ }
92
+ const key = normalizePath(resolved);
93
+ if (visited.has(key) || key.includes('/node_modules/')) {
94
+ continue;
95
+ }
96
+ visited.add(key);
97
+ if (isDeclarationFile(resolved) || !isScannable(resolved)) {
98
+ continue;
99
+ }
100
+ collected.push(resolved);
101
+ const childSource = readFileSafe(resolved);
102
+ if (childSource == null) {
103
+ continue;
104
+ }
105
+ // Feed this read into `sources` so the `scan()` call below reuses it
106
+ // instead of hitting disk again — both the scanners' own file reads
107
+ // and the export-table construction behind same-selector
108
+ // disambiguation consult `sources` first.
109
+ sources.set(key, childSource);
110
+ nextFrontier.push({ absPath: resolved, source: childSource });
111
+ }
112
+ }
113
+ frontier = nextFrontier;
114
+ }
115
+ const pretenders = await scan(collected, { sources });
116
+ resultCache.set(entryKey, { sourceCode, pretenders });
117
+ return pretenders;
118
+ }
119
+ function isScannable(filePath) {
120
+ return SCANNABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
121
+ }
122
+ const DECLARATION_SUFFIXES = ['.d.ts', '.d.mts', '.d.cts'];
123
+ function isDeclarationFile(filePath) {
124
+ const lower = filePath.toLowerCase();
125
+ return DECLARATION_SUFFIXES.some(suffix => lower.endsWith(suffix));
126
+ }
127
+ function readFileSafe(filePath) {
128
+ try {
129
+ return fs.readFileSync(filePath, 'utf8');
130
+ }
131
+ catch (error) {
132
+ if (isFatalError(error)) {
133
+ throw error;
134
+ }
135
+ return null;
136
+ }
137
+ }
package/lib/cli.d.ts CHANGED
@@ -1,11 +1 @@
1
- /**
2
- * CLI entry point for the pretenders scanner.
3
- * Accepts file glob patterns as input arguments and writes discovered
4
- * pretender mappings to a JSON output file.
5
- *
6
- * Usage: pretenders [options] <glob patterns...>
7
- * Flags:
8
- * -O, --out <path> Output file path (required)
9
- * --ignore <names> Comma-separated list of component names to ignore
10
- */
11
1
  export {};
package/lib/cli.js CHANGED
@@ -1,13 +1,3 @@
1
- /**
2
- * CLI entry point for the pretenders scanner.
3
- * Accepts file glob patterns as input arguments and writes discovered
4
- * pretender mappings to a JSON output file.
5
- *
6
- * Usage: pretenders [options] <glob patterns...>
7
- * Flags:
8
- * -O, --out <path> Output file path (required)
9
- * --ignore <names> Comma-separated list of component names to ignore
10
- */
11
1
  import path from 'node:path';
12
2
  import meow from 'meow';
13
3
  import { getFileList } from './input.js';
@@ -35,6 +25,6 @@ async function main() {
35
25
  ignoreComponentNames: commands.flags.ignore?.split(',').map(s => s.trim()),
36
26
  });
37
27
  const outFilePath = path.resolve(process.cwd(), commands.flags.out);
38
- await out(outFilePath, pretenders);
28
+ await out(outFilePath, pretenders, process.cwd());
39
29
  }
40
30
  await main();
@@ -1,11 +1,6 @@
1
1
  import type { PretenderScannerScanMethod } from './types.js';
2
2
  import type { PretenderScanOptions } from '@markuplint/ml-config';
3
3
  /**
4
- * Creates a pretender scanner by wrapping a scan method with path validation.
5
- * Ensures all provided file paths are absolute before delegating to the scan method.
6
- *
7
- * @template O - The scan options type, extending PretenderScanOptions
8
- * @param method - The underlying scan method to wrap with validation
9
- * @returns A wrapped scanner function that validates file paths and then invokes the scan method
4
+ * Wraps a scan method to enforce that all file paths are absolute before delegating.
10
5
  */
11
6
  export declare function createScanner<O extends PretenderScanOptions = PretenderScanOptions>(method: PretenderScannerScanMethod<O>): (files: readonly string[], options?: O) => Promise<import("@markuplint/ml-config").Pretender[]>;
@@ -1,11 +1,6 @@
1
1
  import path from 'node:path';
2
2
  /**
3
- * Creates a pretender scanner by wrapping a scan method with path validation.
4
- * Ensures all provided file paths are absolute before delegating to the scan method.
5
- *
6
- * @template O - The scan options type, extending PretenderScanOptions
7
- * @param method - The underlying scan method to wrap with validation
8
- * @returns A wrapped scanner function that validates file paths and then invokes the scan method
3
+ * Wraps a scan method to enforce that all file paths are absolute before delegating.
9
4
  */
10
5
  export function createScanner(method) {
11
6
  return (files, options) => {
@@ -1,25 +1,52 @@
1
1
  import type { PretenderDirectorMap } from './pretender-director.js';
2
2
  import type { Identifier } from './types.js';
3
+ import type { ImportBinding } from './import-resolver/types.js';
3
4
  import type { Pretender } from '@markuplint/ml-config';
5
+ export interface DependencyMapperContext {
6
+ /** Import bindings found in each source file, keyed by the same relative path used as `sourceFile`. */
7
+ readonly importsByFile?: ReadonlyMap<string, readonly ImportBinding[]>;
8
+ /** Base directory that relative `sourceFile` paths (and resolved module paths) are relative to. */
9
+ readonly cwd?: string;
10
+ /**
11
+ * In-memory content overrides (keyed by normalized absolute path), consulted
12
+ * before falling back to disk when resolving a chased import's export table.
13
+ * Without this, a file with unsaved content (e.g. the lint target itself,
14
+ * when reached via a circular import) resolves against its stale on-disk
15
+ * version instead — which can silently pick a wrong same-named component
16
+ * (see the module JSDoc on `getExportTableForFile`).
17
+ */
18
+ readonly sources?: ReadonlyMap<string, string>;
19
+ }
4
20
  /**
5
- * Resolves a map of component-to-identity mappings into a flat array of Pretender objects.
6
21
  * Follows chains where one component wraps another (e.g., MyButton -> Button -> button)
7
22
  * until a native element is reached or a cycle is detected.
8
23
  *
9
- * Uses import-path-based resolution when a name index is provided, falling back to
10
- * name-based lookup for backward compatibility.
24
+ * Resolution order at each hop:
25
+ * 1. A same-file local declaration (the file the reference itself lives in).
26
+ * 2. The file's recorded import bindings, resolved to the actual declaration file
27
+ * via TypeScript module resolution and the target file's export table.
28
+ * 3. The legacy name-based index, for backward compatibility when no file context
29
+ * is available (or the reference can't be resolved through it).
11
30
  *
12
- * @param map - The map of component keys to their [identifier, identity, filePath] tuples
13
- * @param nameIndex - Optional mapping from component names to map keys for resolving references
14
- * @returns A sorted array of fully resolved Pretender objects
31
+ * (1) and (2) are what let same-named components declared in different files
32
+ * (e.g., two unrelated `Item` components) resolve independently instead of the
33
+ * first-registered one silently winning for every reference see issue #3951.
15
34
  */
16
- export declare function dependencyMapper(map: Readonly<PretenderDirectorMap>, nameIndex?: Readonly<Map<Identifier, string>>): Pretender[];
35
+ export declare function dependencyMapper(map: Readonly<PretenderDirectorMap>, nameIndex?: Readonly<Map<Identifier, string>>, context?: DependencyMapperContext): Pretender[];
17
36
  /**
18
- * Creates a comparator function that sorts objects by a specified property.
37
+ * Clears the module-level export-table cache. A table built from a file's
38
+ * on-disk content never expires on its own, so a long-running host (a
39
+ * watch-mode lint run, an editor extension) that keeps resolving pretenders
40
+ * across file edits must call this whenever it re-resolves without cache (e.g.
41
+ * after a file change) — otherwise a renamed or restructured export keeps
42
+ * resolving to what it used to be for the rest of the process's lifetime.
19
43
  *
20
- * @template T - The object type
21
- * @template P - The property key type
22
- * @param propName - The property to sort by (case-insensitive for strings)
23
- * @returns A comparator function for use with `Array.prototype.sort()`
44
+ * Tables built from an in-memory `sources` override are the exception: their
45
+ * text is already in memory, so comparing it costs nothing and they rebuild as
46
+ * soon as it differs.
24
47
  */
25
- export declare function propSort<T, P extends keyof T>(propName: P): (a: T, b: T) => 1 | -1 | 0;
48
+ export declare function clearExportTableCache(): void;
49
+ /**
50
+ * Comparator that sorts by `propName`, case-insensitive for string values.
51
+ */
52
+ export declare function propSort<T, P extends keyof T>(propName: P): (a: T, b: T) => 0 | 1 | -1;