@markuplint/pretenders 5.0.0-dev.5 → 5.0.0-rc.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +207 -62
  3. package/lib/cli.js +2 -3
  4. package/lib/dependency-mapper.d.ts +18 -35
  5. package/lib/dependency-mapper.js +40 -50
  6. package/lib/import-resolver/extract-script-source.d.ts +82 -0
  7. package/lib/import-resolver/extract-script-source.js +227 -0
  8. package/lib/import-resolver/index.d.ts +85 -0
  9. package/lib/import-resolver/index.js +184 -0
  10. package/lib/import-resolver/parse-imports.d.ts +19 -0
  11. package/lib/import-resolver/parse-imports.js +194 -0
  12. package/lib/import-resolver/resolve-barrel.d.ts +19 -0
  13. package/lib/import-resolver/resolve-barrel.js +113 -0
  14. package/lib/import-resolver/types.d.ts +34 -0
  15. package/lib/import-resolver/types.js +1 -0
  16. package/lib/index.d.ts +26 -1
  17. package/lib/index.js +24 -1
  18. package/lib/jsx/create-identify.d.ts +3 -4
  19. package/lib/jsx/create-identify.js +7 -22
  20. package/lib/jsx/get-children.d.ts +12 -6
  21. package/lib/jsx/get-children.js +64 -8
  22. package/lib/jsx/index.d.ts +4 -0
  23. package/lib/jsx/index.js +13 -5
  24. package/lib/pretender-director.d.ts +13 -4
  25. package/lib/pretender-director.js +15 -6
  26. package/lib/scan.d.ts +20 -0
  27. package/lib/scan.js +24 -0
  28. package/lib/template/derive-name.d.ts +12 -0
  29. package/lib/template/derive-name.js +27 -0
  30. package/lib/template/detect-slots.d.ts +14 -0
  31. package/lib/template/detect-slots.js +23 -0
  32. package/lib/template/extract-attrs.d.ts +13 -0
  33. package/lib/template/extract-attrs.js +26 -0
  34. package/lib/template/extract-root.d.ts +11 -0
  35. package/lib/template/extract-root.js +17 -0
  36. package/lib/template/index.d.ts +13 -0
  37. package/lib/template/index.js +51 -0
  38. package/lib/template/parse-component.d.ts +22 -0
  39. package/lib/template/parse-component.js +74 -0
  40. package/lib/template/types.d.ts +6 -0
  41. package/lib/template/types.js +1 -0
  42. package/lib/types.d.ts +7 -0
  43. package/package.json +11 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,31 @@
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.0](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.3...v5.0.0-rc.0) (2026-03-12)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **pretenders,ml-config:** add scan field to JSON schema and narrow getParser error handling ([e5fda17](https://github.com/markuplint/markuplint/commit/e5fda171f886c1b65d6f0e536c182cebb279ea07))
11
+ - **pretenders:** add error handling to parseComponent and resolveBarrelExport ([c203c4b](https://github.com/markuplint/markuplint/commit/c203c4be341c89abfd0ba7991f09d8322d17728e))
12
+ - **pretenders:** address QA review findings for import resolver phase 2 ([d8fd73f](https://github.com/markuplint/markuplint/commit/d8fd73f533e9b1febec05d93f144f2d5122cffc9))
13
+ - **pretenders:** align import-resolver with [#3335](https://github.com/markuplint/markuplint/issues/3335) design spec ([448181a](https://github.com/markuplint/markuplint/commit/448181ac819e279c925e68c8f5577c7a7cd8ae61)), closes [#3339](https://github.com/markuplint/markuplint/issues/3339) [#3340](https://github.com/markuplint/markuplint/issues/3340)
14
+ - **pretenders:** fix ensureInit TOCTOU race condition ([d9643d8](https://github.com/markuplint/markuplint/commit/d9643d8b679e888b143d9d47f243e35fbeeb6752))
15
+ - **pretenders:** fix false positives in children detection and harden tests ([96f7af3](https://github.com/markuplint/markuplint/commit/96f7af3adf4d6a3b8c65a9e9464cf1bf34c48613))
16
+ - **pretenders:** guard dynamic parser imports and improve test coverage ([9667aa1](https://github.com/markuplint/markuplint/commit/9667aa1278a3ac737fa8c6cfc2df5c1ffd9834c9))
17
+ - **pretenders:** handle empty path edge case in deriveName ([0613743](https://github.com/markuplint/markuplint/commit/0613743c3a83c124b90a5e3f54c62e23b2a7b3bd))
18
+ - **pretenders:** pass importPath in both scanners to prevent name collision ([8644b21](https://github.com/markuplint/markuplint/commit/8644b21b99f1a5a13ab58a4d6d042d2e1aa2a446))
19
+ - **pretenders:** rename createIndentity to createIdentity ([e8f37e5](https://github.com/markuplint/markuplint/commit/e8f37e52f32107e7cc9a9415d2edb6b3bb442fe5))
20
+ - **pretenders:** warn when parser package is not found ([03d567e](https://github.com/markuplint/markuplint/commit/03d567e937228ecfd5b3d9d7811814efd9dd2d09))
21
+ - use visited set for cycle detection in dependencyMapper ([8821f4f](https://github.com/markuplint/markuplint/commit/8821f4fab9cb900582ae0f991e5efe7be413d584)), closes [#3336](https://github.com/markuplint/markuplint/issues/3336)
22
+
23
+ ### Features
24
+
25
+ - **pretenders,ml-core:** implement slots detection in JSX scanner and ml-core consumption ([ad9c8e2](https://github.com/markuplint/markuplint/commit/ad9c8e20d233cddc752fce9ad83838857f81787f)), closes [#3341](https://github.com/markuplint/markuplint/issues/3341)
26
+ - **pretenders:** add import-resolver module via es-module-lexer ([19c9f65](https://github.com/markuplint/markuplint/commit/19c9f65b3856613fa2d7bc59cc79d5b829894663)), closes [#3339](https://github.com/markuplint/markuplint/issues/3339)
27
+ - **pretenders:** add MLAST-based templateScanner for Vue/Svelte/Astro ([b710639](https://github.com/markuplint/markuplint/commit/b71063937bd13523a7fef31da2c2f9095674a957)), closes [#3338](https://github.com/markuplint/markuplint/issues/3338)
28
+ - **pretenders:** dispatch CLI input to both JSX and template scanners ([a5535af](https://github.com/markuplint/markuplint/commit/a5535af1e7e496f570cddce52148eb21f9611cfe))
29
+ - **pretenders:** import resolver phase 2 — dynamic imports, Vue Options API, barrel files ([203d4fb](https://github.com/markuplint/markuplint/commit/203d4fb5bfdc0656f95a39af23b5e079ea324d39)), closes [#3359](https://github.com/markuplint/markuplint/issues/3359)
30
+
6
31
  # [5.0.0-alpha.3](https://github.com/markuplint/markuplint/compare/v5.0.0-alpha.2...v5.0.0-alpha.3) (2026-02-26)
7
32
 
8
33
  **Note:** Version bump only for package @markuplint/pretenders
package/README.md CHANGED
@@ -1,18 +1,59 @@
1
1
  # @markuplint/pretenders
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/%40markuplint%2Fpretenders.svg)](https://www.npmjs.com/package/@markuplint/pretenders)
4
- [![Build Status](https://travis-ci.org/markuplint/markuplint.svg?branch=main)](https://travis-ci.org/markuplint/markuplint)
5
- [![Coverage Status](https://coveralls.io/repos/github/markuplint/markuplint/badge.svg?branch=main)](https://coveralls.io/github/markuplint/markuplint?branch=main)
6
4
 
7
- This module features both an API and a CLI that generate **[Pretenders](https://markuplint.dev/docs/guides/besides-html#pretenders) data** from the loaded components
5
+ This module features both an API and a CLI that generate **[Pretenders](https://markuplint.dev/docs/guides/besides-html#pretenders) data** from the loaded components.
8
6
 
9
- ## Usage
7
+ ## Supported Frameworks
8
+
9
+ | Framework | Extensions | Scanner | Approach |
10
+ | ----------- | ---------------------------- | ----------------- | ------------------------------------- |
11
+ | React / JSX | `.js`, `.jsx`, `.ts`, `.tsx` | `jsxScanner` | TypeScript compiler API |
12
+ | Vue | `.vue` | `templateScanner` | MLAST via `@markuplint/vue-parser` |
13
+ | Svelte | `.svelte` | `templateScanner` | MLAST via `@markuplint/svelte-parser` |
14
+ | Astro | `.astro` | `templateScanner` | MLAST via `@markuplint/astro-parser` |
15
+
16
+ ## CLI Usage
10
17
 
11
18
  ```sh
12
- $ npx @markuplint/pretenders "./src/**/*.jsx" --out "./pretenders.json"
19
+ $ npx @markuplint/pretenders "./src/**/*.{jsx,tsx,vue,svelte,astro}" --out "./pretenders.json"
13
20
  ```
14
21
 
15
- The module analyzes components defined in files using a parser, currently supporting JSX (both `*.jsx` and `*.tsx` formats). It searches for functions or function objects that return elements and maps their function names or the variable names holding these function objects. For example, if a function object named `Foo` returns a `<div>`, the component `Foo` is considered as pretending to be a `div`. In the CLI, it exports the mapped data as a JSON file. By loading this JSON file into the Pretenders feature, the module evaluates the `Foo` component as equivalent to a `div`.
22
+ The CLI accepts glob patterns covering any combination of the supported frameworks. It dispatches files to the appropriate scanner based on file extension, runs them in parallel, and writes the merged results as JSON.
23
+
24
+ | Flag | Description |
25
+ | ------------- | -------------------------------------------------- |
26
+ | `-O`, `--out` | Output file path (required) |
27
+ | `--ignore` | Comma-separated list of component names to exclude |
28
+
29
+ ### Configuration-based Scanning
30
+
31
+ Instead of the CLI, you can configure dynamic scanning directly in your markuplint config file. The `scan` field in `pretenders` accepts glob patterns and automatically dispatches to the appropriate scanner. The `files` field accepts either a single glob string or an array of globs:
32
+
33
+ ```jsonc
34
+ // .markuplintrc
35
+ {
36
+ "pretenders": {
37
+ "scan": [
38
+ {
39
+ // Single glob string
40
+ "files": "./src/components/**/*.{vue,tsx,svelte,astro}",
41
+ "ignoreComponentNames": ["InternalHelper"],
42
+ },
43
+ {
44
+ // Array of glob strings
45
+ "files": ["./src/pages/**/*.tsx", "./src/layouts/**/*.astro"],
46
+ },
47
+ ],
48
+ },
49
+ }
50
+ ```
51
+
52
+ ## How It Works
53
+
54
+ ### JSX Scanner
55
+
56
+ The JSX scanner analyzes components defined in files using the TypeScript compiler API. It searches for functions or function objects that return elements and maps their function names or the variable names holding these function objects. For example, if a function object named `Foo` returns a `<div>`, the component `Foo` is considered as pretending to be a `div`.
16
57
 
17
58
  ```jsx
18
59
  const Foo = () => <div />;
@@ -24,20 +65,12 @@ function Bar() {
24
65
 
25
66
  ```json
26
67
  [
27
- {
28
- "selector": "Foo",
29
- "as": "div"
30
- },
31
- {
32
- "selector": "Bar",
33
- "as": "span"
34
- }
68
+ { "selector": "Foo", "as": "div" },
69
+ { "selector": "Bar", "as": "span" }
35
70
  ]
36
71
  ```
37
72
 
38
- The module is **experimental**. It uses the TypeScript compiler to identify functions or function objects in JSX files where the return values are components or HTML elements. Currently, it only performs a simplistic mapping based on function and variable names without considering dependencies between files. **Consequently, it does not handle name duplications across files or variable scopes;** components with duplicate names overwrite existing data during processing.
39
-
40
- In addition to definitions based on function and variable names, the module also infers HTML elements from properties, as exemplified by `styled-components`, and infers dependencies from arguments.
73
+ The JSX scanner also infers HTML elements from styled-components patterns and infers dependencies from wrapper function arguments:
41
74
 
42
75
  ```jsx
43
76
  const Foo = styled.div`
@@ -49,90 +82,202 @@ const Bar = styled(Foo)`
49
82
  `;
50
83
  ```
51
84
 
85
+ ```json
86
+ [
87
+ { "selector": "Foo", "as": "div" },
88
+ { "selector": "Bar", "as": "div" }
89
+ ]
90
+ ```
91
+
92
+ The JSX scanner detects **slots** (children). If a component accepts `children` props, the resulting pretender includes `slots: true` in its `as` field.
93
+
94
+ ### Template Scanner
95
+
96
+ The template scanner uses markuplint's own framework parsers (Vue, Svelte, Astro) to extract the root element from component templates at depth=0. It also detects static attributes and slot/children usage.
97
+
98
+ ```vue
99
+ <template>
100
+ <button type="submit"><slot /></button>
101
+ </template>
102
+ ```
103
+
52
104
  ```json
53
105
  [
54
106
  {
55
- "selector": "Foo",
56
- "as": "div"
57
- },
58
- {
59
- "selector": "Bar",
60
- "as": "div"
107
+ "selector": "SubmitButton",
108
+ "as": {
109
+ "element": "button",
110
+ "attrs": [{ "name": "type", "value": "submit" }],
111
+ "slots": true
112
+ }
61
113
  }
62
114
  ]
63
115
  ```
64
116
 
117
+ Slot detection covers:
118
+
119
+ - `<slot>` elements in Vue, Svelte, and Astro
120
+ - `{@render children()}` snippets in Svelte 5
121
+
122
+ ### Import Resolver
123
+
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
+ Supported script block types:
127
+
128
+ - Vue `<script setup>` (all static imports are exposed as bindings)
129
+ - Vue Options API `<script>` (fallback when no `<script setup>`; only imports registered in `components: { ... }` are returned)
130
+ - Svelte `<script>` (prefers instance script over module script)
131
+ - Astro frontmatter (`---...---`)
132
+ - MDX top-level ESM
133
+
134
+ Dynamic imports with string literal specifiers (`import('./path')`) are included in bindings with `type: 'dynamic'`. Template literal and variable specifiers are excluded.
135
+
136
+ 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
+
65
138
  ## API
66
139
 
140
+ ### `scan(files, options)`
141
+
142
+ The unified entry point. Dispatches files to the appropriate scanner based on file extension, runs both scanners in parallel, and returns the merged, sorted results.
143
+
144
+ ```ts
145
+ import { scan } from '@markuplint/pretenders';
146
+
147
+ const pretenders = await scan([
148
+ '/absolute/path/to/Button.tsx',
149
+ '/absolute/path/to/Card.vue',
150
+ '/absolute/path/to/Alert.svelte',
151
+ ]);
152
+ ```
153
+
154
+ #### Parameters
155
+
156
+ | Parameter | Type | Description |
157
+ | ------------------------------ | ------------------- | --------------------------------------- |
158
+ | `files` | `readonly string[]` | Absolute file paths to scan |
159
+ | `options.ignoreComponentNames` | `readonly string[]` | Component names to exclude from results |
160
+
67
161
  ### `jsxScanner(files, options)`
68
162
 
163
+ Scans JSX/TSX files using the TypeScript compiler API.
164
+
69
165
  ```ts
70
166
  import { jsxScanner } from '@markuplint/pretenders';
71
167
 
72
- const pretenders = jsxScanner(['./src/**/*.jsx'], {
168
+ const pretenders = await jsxScanner(['/absolute/path/to/Component.jsx'], {
73
169
  cwd: process.cwd(),
74
170
  asFragment: [/(?:^|\.)provider$/i],
75
171
  ignoreComponentNames: [],
76
- taggedStylingComponent: [
77
- // PropertyAccessExpression: styled.button`css-prop: value;`
78
- /^styled\.(?<tagName>[a-z][\da-z]*)$/i,
79
- // CallExpression: styled(Button)`css-prop: value;`
80
- /^styled\s*\(\s*(?<tagName>[a-z][\da-z]*)\s*\)$/i,
81
- ],
172
+ taggedStylingComponent: [/^styled\.(?<tagName>[a-z][\da-z]*)$/i, /^styled\s*\(\s*(?<tagName>[a-z][\da-z]*)\s*\)$/i],
82
173
  extendingWrapper: [],
83
174
  });
84
175
  ```
85
176
 
86
- #### `files`
177
+ #### Parameters
87
178
 
88
- Type: `string[]`
179
+ | Parameter | Type | Description |
180
+ | -------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------- |
181
+ | `files` | `readonly string[]` | Absolute file paths to scan |
182
+ | `options.cwd` | `string` | Current working directory |
183
+ | `options.asFragment` | `readonly (RegExp \| string)[]` | Patterns for components treated as transparent fragments |
184
+ | `options.ignoreComponentNames` | `readonly string[]` | Component names to ignore |
185
+ | `options.taggedStylingComponent` | `readonly (RegExp \| string)[]` | Patterns for styled-components tagged template expressions |
186
+ | `options.extendingWrapper` | `readonly (string \| RegExp \| ExtendingWrapperCallerOptions)[]` | Patterns for HOC/wrapper components |
89
187
 
90
- An array of file paths to scan.
188
+ ### `templateScanner(files, options)`
91
189
 
92
- ##### `options.cwd`
190
+ Scans Vue, Svelte, and Astro component files using markuplint's own parsers (MLAST-based).
93
191
 
94
- Type: `string`
192
+ ```ts
193
+ import { templateScanner } from '@markuplint/pretenders';
95
194
 
96
- The current working directory.
195
+ const pretenders = await templateScanner(
196
+ ['/absolute/path/to/Button.vue', '/absolute/path/to/Alert.svelte', '/absolute/path/to/Card.astro'],
197
+ {
198
+ ignoreComponentNames: ['InternalHelper'],
199
+ },
200
+ );
201
+ ```
202
+
203
+ #### Parameters
97
204
 
98
- ##### `options.asFragment`
205
+ | Parameter | Type | Description |
206
+ | ------------------------------ | ------------------- | ---------------------------------------------- |
207
+ | `files` | `readonly string[]` | Absolute file paths to scan |
208
+ | `options.cwd` | `string` | Current working directory (for relative paths) |
209
+ | `options.ignoreComponentNames` | `readonly string[]` | Component names to exclude from results |
99
210
 
100
- Type: `RegExp[]`
211
+ ### `analyzeImports(filePath, source)`
101
212
 
102
- A list of regular expressions to match components that should be treated as fragments.
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.
103
214
 
104
- ##### `options.ignoreComponentNames`
215
+ Returns `null` if the file extension is not a supported framework (`.vue`, `.svelte`, `.astro`, `.mdx`).
105
216
 
106
- Type: `string[]`
217
+ ```ts
218
+ import { analyzeImports } from '@markuplint/pretenders';
219
+
220
+ const result = await analyzeImports('App.vue', source);
221
+ // result.bindings: [{ localName: 'MyButton', importedName: 'default', source: './components/MyButton.vue', type: 'default' }, ...]
222
+ ```
107
223
 
108
- A list of component names to ignore.
224
+ #### Parameters
109
225
 
110
- ##### `options.taggedStylingComponent`
226
+ | Parameter | Type | Description |
227
+ | ---------- | -------- | ----------------------------------------------------- |
228
+ | `filePath` | `string` | File path (used for framework detection by extension) |
229
+ | `source` | `string` | Full source text of the component file |
111
230
 
112
- Type: `RegExp[]`
231
+ #### Returns
113
232
 
114
- A list of regular expressions to match components that are styled.
233
+ `Promise<ImportAnalysisResult | null>` The analysis result with all import bindings, or `null` if the framework is not supported.
115
234
 
116
- ##### `options.extendingWrapper`
235
+ ### `resolveComponentImport(componentName, bindings)`
117
236
 
118
- Type: `RegExp[]` | `{ identifier: RegExp, numberOfArgument: number }[]`
237
+ Resolves a component name used in a template to its import binding. Handles Vue's kebab-case to PascalCase normalization (e.g., `<my-button>` resolves to `MyButton`).
119
238
 
120
- ```js
121
- jsxScanner(['./src/**/*.jsx'], {
122
- extendingWrapper: [
123
- {
124
- identifier: /^namespace\.primary$/i,
125
- numberOfArgument: 1,
126
- },
127
- ],
128
- });
239
+ ```ts
240
+ import { resolveComponentImport } from '@markuplint/pretenders';
241
+
242
+ const binding = resolveComponentImport('my-button', bindings);
243
+ // binding: { localName: 'MyButton', importedName: 'default', source: './components/MyButton.vue', type: 'default' }
129
244
  ```
130
245
 
131
- ```jsx
132
- const Foo = <div />;
133
- const Bar = namespace.primary(true, Foo);
246
+ #### Parameters
247
+
248
+ | Parameter | Type | Description |
249
+ | --------------- | -------------------------- | -------------------------------------- |
250
+ | `componentName` | `string` | Component name as used in the template |
251
+ | `bindings` | `readonly ImportBinding[]` | Import bindings from `analyzeImports` |
252
+
253
+ #### Returns
254
+
255
+ `ImportBinding | undefined` — The matching binding, or `undefined` if no match.
256
+
257
+ ### `resolveBarrelExport(specifier, importedName, importerPath)`
258
+
259
+ Resolves a barrel file (`index.ts`/`index.js`) re-export to the original source module path. Only handles relative specifiers and single-level barrel resolution.
260
+
261
+ ```ts
262
+ import { analyzeImports, resolveComponentImport, resolveBarrelExport } from '@markuplint/pretenders';
263
+
264
+ const result = await analyzeImports('App.vue', source);
265
+ const binding = resolveComponentImport('Button', result.bindings);
266
+
267
+ if (binding) {
268
+ const originalSource = resolveBarrelExport(binding.source, binding.importedName, '/absolute/path/to/App.vue');
269
+ // originalSource: './Button.vue' (resolved from './components' barrel)
270
+ }
134
271
  ```
135
272
 
136
- A list of regular expressions to match components that are extended.
137
- `identifier` is a regular expression to match the component name.
138
- `numberOfArgument` is the number of arguments to pass to the component.
273
+ #### Parameters
274
+
275
+ | Parameter | Type | Description |
276
+ | -------------- | -------- | ----------------------------------------------- |
277
+ | `specifier` | `string` | The import specifier (e.g., `'./components'`) |
278
+ | `importedName` | `string` | The name being imported (e.g., `'Button'`) |
279
+ | `importerPath` | `string` | Absolute path of the file containing the import |
280
+
281
+ #### Returns
282
+
283
+ `string | null` — The relative source path from the barrel file, or `null` if not a barrel or name not found.
package/lib/cli.js CHANGED
@@ -11,8 +11,8 @@
11
11
  import path from 'node:path';
12
12
  import meow from 'meow';
13
13
  import { getFileList } from './input.js';
14
- import { jsxScanner } from './jsx/index.js';
15
14
  import { out } from './out.js';
15
+ import { scan } from './scan.js';
16
16
  const commands = meow({
17
17
  importMeta: import.meta,
18
18
  flags: {
@@ -31,8 +31,7 @@ if (commands.input.length === 0) {
31
31
  }
32
32
  async function main() {
33
33
  const files = await getFileList(commands.input);
34
- const jsxFiles = files.filter(filePath => /\.[jt]sx?$/.test(filePath));
35
- const pretenders = await jsxScanner(jsxFiles, {
34
+ const pretenders = await scan(files, {
36
35
  ignoreComponentNames: commands.flags.ignore?.split(',').map(s => s.trim()),
37
36
  });
38
37
  const outFilePath = path.resolve(process.cwd(), commands.flags.out);
@@ -1,42 +1,25 @@
1
- import type { Identifier, Identity } from './types.js';
1
+ import type { PretenderDirectorMap } from './pretender-director.js';
2
+ import type { Identifier } from './types.js';
2
3
  import type { Pretender } from '@markuplint/ml-config';
3
- /**
4
- * Internal map structure storing component identifiers to their identity and source location.
5
- */
6
- type PretenderDirectorMap = Map<Identifier, [identity: Identity, filePath?: string]>;
7
- /**
8
- * Collects and manages pretender mappings discovered during source file scanning.
9
- * Acts as a registry where component-to-element relationships are added during
10
- * traversal, then resolved into a flat list of pretenders with dependency linking.
11
- */
12
- export declare class PretenderDirector {
13
- #private;
14
- /**
15
- * Registers a component as a pretender mapping. If the identifier is already
16
- * registered, the call is silently ignored (first definition wins).
17
- *
18
- * @param identifier - The component selector (e.g., component name)
19
- * @param identity - The native HTML element the component renders as
20
- * @param filePath - The relative file path where the component is defined
21
- * @param line - The line number of the component declaration
22
- * @param col - The column number of the component declaration
23
- */
24
- add(identifier: Identifier, identity: Identity, filePath: string, line: number, col: number): void;
25
- /**
26
- * Resolves all registered mappings into a sorted array of Pretender objects.
27
- * Follows component-to-component chains to determine the final native element identity.
28
- *
29
- * @returns A sorted array of resolved Pretender objects
30
- */
31
- getPretenders(): Pretender[];
32
- }
33
4
  /**
34
5
  * Resolves a map of component-to-identity mappings into a flat array of Pretender objects.
35
6
  * Follows chains where one component wraps another (e.g., MyButton -> Button -> button)
36
- * until a native element is reached or a recursive loop is detected.
7
+ * until a native element is reached or a cycle is detected.
37
8
  *
38
- * @param map - The map of component identifiers to their identity and file path
9
+ * Uses import-path-based resolution when a name index is provided, falling back to
10
+ * name-based lookup for backward compatibility.
11
+ *
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
39
14
  * @returns A sorted array of fully resolved Pretender objects
40
15
  */
41
- export declare function dependencyMapper(map: Readonly<PretenderDirectorMap>): Pretender[];
42
- export {};
16
+ export declare function dependencyMapper(map: Readonly<PretenderDirectorMap>, nameIndex?: Readonly<Map<Identifier, string>>): Pretender[];
17
+ /**
18
+ * Creates a comparator function that sorts objects by a specified property.
19
+ *
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()`
24
+ */
25
+ export declare function propSort<T, P extends keyof T>(propName: P): (a: T, b: T) => 1 | -1 | 0;
@@ -1,89 +1,80 @@
1
- /**
2
- * Collects and manages pretender mappings discovered during source file scanning.
3
- * Acts as a registry where component-to-element relationships are added during
4
- * traversal, then resolved into a flat list of pretenders with dependency linking.
5
- */
6
- export class PretenderDirector {
7
- #map = new Map();
8
- /**
9
- * Registers a component as a pretender mapping. If the identifier is already
10
- * registered, the call is silently ignored (first definition wins).
11
- *
12
- * @param identifier - The component selector (e.g., component name)
13
- * @param identity - The native HTML element the component renders as
14
- * @param filePath - The relative file path where the component is defined
15
- * @param line - The line number of the component declaration
16
- * @param col - The column number of the component declaration
17
- */
18
- add(identifier, identity, filePath, line, col) {
19
- if (this.#map.has(identifier)) {
20
- return;
21
- }
22
- this.#map.set(identifier, [identity, `${filePath}:${line}:${col}`]);
23
- }
24
- /**
25
- * Resolves all registered mappings into a sorted array of Pretender objects.
26
- * Follows component-to-component chains to determine the final native element identity.
27
- *
28
- * @returns A sorted array of resolved Pretender objects
29
- */
30
- getPretenders() {
31
- return dependencyMapper(this.#map);
32
- }
33
- }
34
1
  /**
35
2
  * Resolves a map of component-to-identity mappings into a flat array of Pretender objects.
36
3
  * Follows chains where one component wraps another (e.g., MyButton -> Button -> button)
37
- * until a native element is reached or a recursive loop is detected.
4
+ * until a native element is reached or a cycle is detected.
38
5
  *
39
- * @param map - The map of component identifiers to their identity and file path
6
+ * Uses import-path-based resolution when a name index is provided, falling back to
7
+ * name-based lookup for backward compatibility.
8
+ *
9
+ * @param map - The map of component keys to their [identifier, identity, filePath] tuples
10
+ * @param nameIndex - Optional mapping from component names to map keys for resolving references
40
11
  * @returns A sorted array of fully resolved Pretender objects
41
12
  */
42
- export function dependencyMapper(map) {
13
+ export function dependencyMapper(map, nameIndex) {
14
+ const resolvedNameIndex = nameIndex ?? buildNameIndex(map);
43
15
  const linkedPretenders = [];
44
- const collection = [...map.entries()];
45
- for (const [identifier, [_identity, _filePath]] of collection) {
16
+ for (const [key, [identifier, _identity, _filePath]] of map) {
46
17
  let identity = _identity;
47
18
  let filePath = _filePath;
48
19
  let elName = getElName(identity);
49
20
  const via = [];
21
+ const visited = new Set([key]);
50
22
  while (true) {
51
- const mappedPretender = map.get(elName);
23
+ const lookupKey = resolvedNameIndex.get(elName) ?? elName;
24
+ const mappedPretender = map.get(lookupKey);
52
25
  if (!mappedPretender) {
53
26
  break;
54
27
  }
55
- identity = mappedPretender[0];
56
- filePath = mappedPretender[1];
57
- if (elName === identifier) {
28
+ identity = mappedPretender[1];
29
+ filePath = mappedPretender[2];
30
+ if (visited.has(lookupKey)) {
58
31
  via.push('...[Recursive]');
59
32
  break;
60
33
  }
34
+ visited.add(lookupKey);
61
35
  via.push(elName);
62
36
  elName = getElName(identity);
63
37
  }
64
38
  const pretender = {
65
39
  selector: identifier,
66
40
  as: identity,
41
+ ...(filePath ? { filePath } : {}),
67
42
  };
68
- if (filePath) {
69
- // @ts-ignore initialize readonly property
70
- pretender.filePath = filePath;
71
- }
72
43
  if (via.length > 0) {
73
- // @ts-ignore
74
- pretender._via = via;
44
+ Object.assign(pretender, { _via: via });
75
45
  }
76
46
  linkedPretenders.push(pretender);
77
47
  }
78
48
  return linkedPretenders.toSorted(propSort('selector'));
79
49
  }
50
+ /**
51
+ * Builds a name-to-key index from the map for backward-compatible name-based lookup.
52
+ * First definition wins when multiple entries share the same identifier.
53
+ */
54
+ function buildNameIndex(map) {
55
+ const index = new Map();
56
+ for (const [key, [identifier]] of map) {
57
+ if (!index.has(identifier)) {
58
+ index.set(identifier, key);
59
+ }
60
+ }
61
+ return index;
62
+ }
80
63
  function getElName(identity) {
81
64
  if (typeof identity === 'string') {
82
65
  return identity;
83
66
  }
84
67
  return identity.element;
85
68
  }
86
- function propSort(propName) {
69
+ /**
70
+ * Creates a comparator function that sorts objects by a specified property.
71
+ *
72
+ * @template T - The object type
73
+ * @template P - The property key type
74
+ * @param propName - The property to sort by (case-insensitive for strings)
75
+ * @returns A comparator function for use with `Array.prototype.sort()`
76
+ */
77
+ export function propSort(propName) {
87
78
  return (a, b) => {
88
79
  const nameA = toLowerCase(a[propName]);
89
80
  const nameB = toLowerCase(b[propName]);
@@ -98,7 +89,6 @@ function propSort(propName) {
98
89
  }
99
90
  function toLowerCase(value) {
100
91
  if (typeof value === 'string') {
101
- // @ts-ignore
102
92
  return value.toLowerCase();
103
93
  }
104
94
  return value;