@markuplint/pretenders 5.0.0-dev.5 → 5.0.0-rc.1

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 (47) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +207 -62
  3. package/lib/cli.js +2 -3
  4. package/lib/component-scanner.d.ts +67 -0
  5. package/lib/component-scanner.js +9 -0
  6. package/lib/dependency-mapper.d.ts +18 -35
  7. package/lib/dependency-mapper.js +40 -50
  8. package/lib/import-resolver/extract-script-source.d.ts +59 -0
  9. package/lib/import-resolver/extract-script-source.js +143 -0
  10. package/lib/import-resolver/index.d.ts +55 -0
  11. package/lib/import-resolver/index.js +152 -0
  12. package/lib/import-resolver/parse-imports.d.ts +19 -0
  13. package/lib/import-resolver/parse-imports.js +194 -0
  14. package/lib/import-resolver/resolve-barrel.d.ts +19 -0
  15. package/lib/import-resolver/resolve-barrel.js +113 -0
  16. package/lib/import-resolver/types.d.ts +34 -0
  17. package/lib/import-resolver/types.js +1 -0
  18. package/lib/index.d.ts +27 -1
  19. package/lib/index.js +24 -1
  20. package/lib/jsx/create-identify.d.ts +3 -4
  21. package/lib/jsx/create-identify.js +7 -22
  22. package/lib/jsx/get-children.d.ts +12 -6
  23. package/lib/jsx/get-children.js +64 -8
  24. package/lib/jsx/index.d.ts +4 -0
  25. package/lib/jsx/index.js +13 -5
  26. package/lib/pretender-director.d.ts +14 -5
  27. package/lib/pretender-director.js +15 -6
  28. package/lib/scan.d.ts +20 -0
  29. package/lib/scan.js +24 -0
  30. package/lib/scanner-loader.d.ts +8 -0
  31. package/lib/scanner-loader.js +56 -0
  32. package/lib/template/derive-name.d.ts +12 -0
  33. package/lib/template/derive-name.js +27 -0
  34. package/lib/template/detect-slots.d.ts +14 -0
  35. package/lib/template/detect-slots.js +23 -0
  36. package/lib/template/extract-attrs.d.ts +13 -0
  37. package/lib/template/extract-attrs.js +26 -0
  38. package/lib/template/extract-root.d.ts +11 -0
  39. package/lib/template/extract-root.js +17 -0
  40. package/lib/template/index.d.ts +13 -0
  41. package/lib/template/index.js +57 -0
  42. package/lib/template/parse-component.d.ts +22 -0
  43. package/lib/template/parse-component.js +74 -0
  44. package/lib/template/types.d.ts +6 -0
  45. package/lib/template/types.js +1 -0
  46. package/lib/types.d.ts +7 -0
  47. package/package.json +12 -5
package/lib/jsx/index.js CHANGED
@@ -11,7 +11,7 @@ import { getPosition } from '@markuplint/parser-utils/location';
11
11
  import ts from 'typescript';
12
12
  import { createScanner } from '../create-scanner.js';
13
13
  import { PretenderDirector } from '../pretender-director.js';
14
- import { createIndentity } from './create-identify.js';
14
+ import { createIdentity } from './create-identify.js';
15
15
  import { finder } from './finder.js';
16
16
  import { getAttributes } from './get-attributes.js';
17
17
  import { getChildren } from './get-children.js';
@@ -43,6 +43,10 @@ const defaultOptions = {
43
43
  * - HOC / wrapper function patterns
44
44
  * - Fragment and provider component transparency
45
45
  * - `@pretends null` JSDoc tag to opt out a component
46
+ *
47
+ * @param files - Absolute file paths to scan (relative paths cause a `ReferenceError`)
48
+ * @param options - JSX scanner configuration (fragment patterns, styled-components, wrappers, etc.)
49
+ * @returns Discovered pretender mappings for all components found in the given files
46
50
  */
47
51
  export const jsxScanner = createScanner((files, options = defaultOptions) => {
48
52
  const { cwd = defaultOptions.cwd, ignoreComponentNames = defaultOptions.ignoreComponentNames, asFragment = defaultOptions.asFragment, taggedStylingComponent = defaultOptions.taggedStylingComponent, extendingWrapper = defaultOptions.extendingWrapper, } = options;
@@ -51,6 +55,10 @@ export const jsxScanner = createScanner((files, options = defaultOptions) => {
51
55
  jsx: JsxEmit.ReactJSX,
52
56
  allowJs: true,
53
57
  });
58
+ // Trigger the binder so that parent pointers are set on AST nodes.
59
+ // getChildren() relies on node.parent to navigate from JsxOpeningElement
60
+ // to its containing JsxElement for children slot detection.
61
+ program.getTypeChecker();
54
62
  for (const sourceFile of program.getSourceFiles()) {
55
63
  if (!sourceFile.isDeclarationFile) {
56
64
  forEachChild(sourceFile, node => visit(node, sourceFile));
@@ -184,7 +192,7 @@ export const jsxScanner = createScanner((files, options = defaultOptions) => {
184
192
  element: tagName,
185
193
  slots: true,
186
194
  inheritAttrs: true,
187
- }, filePath, line, col);
195
+ }, filePath, line, col, `${filePath}#${name}`);
188
196
  }
189
197
  });
190
198
  find(root, isCallExpression, method => {
@@ -229,7 +237,7 @@ export const jsxScanner = createScanner((files, options = defaultOptions) => {
229
237
  element: tagName,
230
238
  slots: true,
231
239
  inheritAttrs: true,
232
- }, filePath, line, col);
240
+ }, filePath, line, col, `${filePath}#${name}`);
233
241
  }
234
242
  });
235
243
  function foundFragment(
@@ -283,8 +291,8 @@ export const jsxScanner = createScanner((files, options = defaultOptions) => {
283
291
  }
284
292
  const attrs = getAttributes(el, sourceFile);
285
293
  const children = getChildren(el, sourceFile);
286
- const identity = createIndentity(tagName, attrs, children);
287
- director.add(name, identity, filePath, line, col);
294
+ const identity = createIdentity(tagName, attrs, children);
295
+ director.add(name, identity, filePath, line, col, `${filePath}#${name}`);
288
296
  }
289
297
  }
290
298
  return Promise.resolve(director.getPretenders());
@@ -1,27 +1,36 @@
1
- import type { Identifier, Identity } from './types.js';
1
+ import type { Identifier, Identity, ImportPath } from './types.js';
2
+ /**
3
+ * Internal map structure storing component mappings keyed by import path (or identifier as fallback).
4
+ * The value tuple includes the component identifier (name), its identity, and an optional source location.
5
+ */
6
+ export type PretenderDirectorMap = Map<string, [identifier: Identifier, identity: Identity, filePath?: string]>;
2
7
  /**
3
8
  * Collects and manages pretender mappings discovered during source file scanning.
4
9
  * Acts as a registry where component-to-element relationships are added during
5
10
  * traversal, then resolved into a flat list of pretenders with dependency linking.
11
+ *
12
+ * The internal map uses import paths as keys when available, falling back to
13
+ * component identifiers (names) for backward compatibility with name-based scanners.
6
14
  */
7
15
  export declare class PretenderDirector {
8
16
  #private;
9
17
  /**
10
- * Registers a component as a pretender mapping. If the identifier is already
11
- * registered, the call is silently ignored (first definition wins).
18
+ * Registers a component as a pretender mapping. If the key (import path or identifier)
19
+ * is already registered, the call is silently ignored (first definition wins).
12
20
  *
13
21
  * @param identifier - The component selector (e.g., component name)
14
22
  * @param identity - The native HTML element the component renders as
15
23
  * @param filePath - The relative file path where the component is defined
16
24
  * @param line - The line number of the component declaration
17
25
  * @param col - The column number of the component declaration
26
+ * @param importPath - Optional import path for uniquely identifying the component across files
18
27
  */
19
- add(identifier: Identifier, identity: Identity, filePath: string, line: number, col: number): void;
28
+ add(identifier: Identifier, identity: Identity, filePath: string, line: number, col: number, importPath?: ImportPath): void;
20
29
  /**
21
30
  * Resolves all registered mappings into a sorted array of Pretender objects.
22
31
  * Follows component-to-component chains to determine the final native element identity.
23
32
  *
24
33
  * @returns A sorted array of resolved Pretender objects
25
34
  */
26
- getPretenders(): import("packages/@markuplint/ml-config/lib/types.js").Pretender[];
35
+ getPretenders(): import("@markuplint/ml-config").Pretender[];
27
36
  }
@@ -3,24 +3,33 @@ import { dependencyMapper } from './dependency-mapper.js';
3
3
  * Collects and manages pretender mappings discovered during source file scanning.
4
4
  * Acts as a registry where component-to-element relationships are added during
5
5
  * traversal, then resolved into a flat list of pretenders with dependency linking.
6
+ *
7
+ * The internal map uses import paths as keys when available, falling back to
8
+ * component identifiers (names) for backward compatibility with name-based scanners.
6
9
  */
7
10
  export class PretenderDirector {
8
11
  #map = new Map();
12
+ #nameIndex = new Map();
9
13
  /**
10
- * Registers a component as a pretender mapping. If the identifier is already
11
- * registered, the call is silently ignored (first definition wins).
14
+ * Registers a component as a pretender mapping. If the key (import path or identifier)
15
+ * is already registered, the call is silently ignored (first definition wins).
12
16
  *
13
17
  * @param identifier - The component selector (e.g., component name)
14
18
  * @param identity - The native HTML element the component renders as
15
19
  * @param filePath - The relative file path where the component is defined
16
20
  * @param line - The line number of the component declaration
17
21
  * @param col - The column number of the component declaration
22
+ * @param importPath - Optional import path for uniquely identifying the component across files
18
23
  */
19
- add(identifier, identity, filePath, line, col) {
20
- if (this.#map.has(identifier)) {
24
+ add(identifier, identity, filePath, line, col, importPath) {
25
+ const key = importPath ?? identifier;
26
+ if (this.#map.has(key)) {
21
27
  return;
22
28
  }
23
- this.#map.set(identifier, [identity, `${filePath}:${line}:${col}`]);
29
+ this.#map.set(key, [identifier, identity, `${filePath}:${line}:${col}`]);
30
+ if (!this.#nameIndex.has(identifier)) {
31
+ this.#nameIndex.set(identifier, key);
32
+ }
24
33
  }
25
34
  /**
26
35
  * Resolves all registered mappings into a sorted array of Pretender objects.
@@ -29,6 +38,6 @@ export class PretenderDirector {
29
38
  * @returns A sorted array of resolved Pretender objects
30
39
  */
31
40
  getPretenders() {
32
- return dependencyMapper(this.#map);
41
+ return dependencyMapper(this.#map, this.#nameIndex);
33
42
  }
34
43
  }
package/lib/scan.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Pretender } from '@markuplint/ml-config';
2
+ /**
3
+ * Options for the unified {@link scan} function.
4
+ */
5
+ export interface ScanOptions {
6
+ /** Component names to exclude from scanning results */
7
+ readonly ignoreComponentNames?: readonly string[];
8
+ }
9
+ /**
10
+ * Dispatches files to the appropriate scanner based on file extension,
11
+ * runs both scanners in parallel, and merges + sorts the results.
12
+ *
13
+ * - `.js`, `.jsx`, `.ts`, `.tsx` → {@link jsxScanner}
14
+ * - `.vue`, `.svelte`, `.astro` → {@link templateScanner} (delegates to parser component-scanners)
15
+ *
16
+ * @param files - Absolute file paths to scan
17
+ * @param options - Optional scan configuration
18
+ * @returns All discovered pretender mappings, sorted by selector
19
+ */
20
+ export declare function scan(files: readonly string[], options?: ScanOptions): Promise<Pretender[]>;
package/lib/scan.js ADDED
@@ -0,0 +1,24 @@
1
+ import { propSort } from './dependency-mapper.js';
2
+ import { jsxScanner } from './jsx/index.js';
3
+ import { templateScanner } from './template/index.js';
4
+ /**
5
+ * Dispatches files to the appropriate scanner based on file extension,
6
+ * runs both scanners in parallel, and merges + sorts the results.
7
+ *
8
+ * - `.js`, `.jsx`, `.ts`, `.tsx` → {@link jsxScanner}
9
+ * - `.vue`, `.svelte`, `.astro` → {@link templateScanner} (delegates to parser component-scanners)
10
+ *
11
+ * @param files - Absolute file paths to scan
12
+ * @param options - Optional scan configuration
13
+ * @returns All discovered pretender mappings, sorted by selector
14
+ */
15
+ export async function scan(files, options) {
16
+ const jsxFiles = files.filter(filePath => /\.[jt]sx?$/.test(filePath));
17
+ const templateFiles = files.filter(filePath => /\.(?:vue|svelte|astro)$/.test(filePath));
18
+ const ignoreComponentNames = options?.ignoreComponentNames ? [...options.ignoreComponentNames] : undefined;
19
+ const [jsxPretenders, templatePretenders] = await Promise.all([
20
+ jsxFiles.length > 0 ? jsxScanner(jsxFiles, { ignoreComponentNames }) : Promise.resolve([]),
21
+ templateFiles.length > 0 ? templateScanner(templateFiles, { ignoreComponentNames }) : Promise.resolve([]),
22
+ ]);
23
+ return [...jsxPretenders, ...templatePretenders].toSorted(propSort('selector'));
24
+ }
@@ -0,0 +1,8 @@
1
+ import type { ComponentScanner } from './component-scanner.js';
2
+ /**
3
+ * Dynamically imports the appropriate component scanner for the given file extension.
4
+ *
5
+ * @param ext - The file extension (e.g., `.vue`, `.svelte`, `.astro`)
6
+ * @returns The component scanner, or `null` if unavailable
7
+ */
8
+ export declare function getScanner(ext: string): Promise<ComponentScanner | null>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Maps file extensions to their corresponding parser package's component-scanner subpath.
3
+ */
4
+ const SCANNER_PACKAGES = {
5
+ '.vue': '@markuplint/vue-parser/component-scanner',
6
+ '.svelte': '@markuplint/svelte-parser/component-scanner',
7
+ '.astro': '@markuplint/astro-parser/component-scanner',
8
+ };
9
+ /**
10
+ * Cache of loaded component scanners.
11
+ */
12
+ const scannerCache = new Map();
13
+ /**
14
+ * Checks if an error is a Node.js ERR_MODULE_NOT_FOUND error.
15
+ *
16
+ * @param error - The caught error value
17
+ * @returns `true` if the error has code `ERR_MODULE_NOT_FOUND`
18
+ */
19
+ function isModuleNotFoundError(error) {
20
+ return (error instanceof Error && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND');
21
+ }
22
+ /**
23
+ * Dynamically imports the appropriate component scanner for the given file extension.
24
+ *
25
+ * @param ext - The file extension (e.g., `.vue`, `.svelte`, `.astro`)
26
+ * @returns The component scanner, or `null` if unavailable
27
+ */
28
+ export async function getScanner(ext) {
29
+ const cached = scannerCache.get(ext);
30
+ if (cached !== undefined) {
31
+ return cached;
32
+ }
33
+ const pkg = SCANNER_PACKAGES[ext];
34
+ if (!pkg) {
35
+ scannerCache.set(ext, null);
36
+ return null;
37
+ }
38
+ try {
39
+ const mod = await import(pkg);
40
+ scannerCache.set(ext, mod.componentScanner);
41
+ return mod.componentScanner;
42
+ }
43
+ catch (error) {
44
+ if (isModuleNotFoundError(error)) {
45
+ const parserPkg = pkg.replace('/component-scanner', '');
46
+ // eslint-disable-next-line no-console
47
+ console.warn(`Parser package "${parserPkg}" is not installed. Skipping ${ext} files.`);
48
+ }
49
+ else {
50
+ // eslint-disable-next-line no-console
51
+ console.warn(`Failed to load component scanner for ${ext}:`, error instanceof Error ? error.message : error);
52
+ }
53
+ scannerCache.set(ext, null);
54
+ return null;
55
+ }
56
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Derives a PascalCase component name from a file path.
3
+ *
4
+ * Rules:
5
+ * - `BaseButton.vue` → `BaseButton` (remove extension)
6
+ * - `base-button.vue` → `BaseButton` (kebab-case → PascalCase)
7
+ * - `Card/index.vue` → `Card` (parent directory name for index files)
8
+ *
9
+ * @param filePath - The component file path to derive a name from
10
+ * @returns The PascalCase component name
11
+ */
12
+ export declare function deriveName(filePath: string): string;
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Derives a PascalCase component name from a file path.
4
+ *
5
+ * Rules:
6
+ * - `BaseButton.vue` → `BaseButton` (remove extension)
7
+ * - `base-button.vue` → `BaseButton` (kebab-case → PascalCase)
8
+ * - `Card/index.vue` → `Card` (parent directory name for index files)
9
+ *
10
+ * @param filePath - The component file path to derive a name from
11
+ * @returns The PascalCase component name
12
+ */
13
+ export function deriveName(filePath) {
14
+ const parsed = path.parse(filePath);
15
+ const baseName = parsed.name;
16
+ const nameToConvert = baseName === 'index' ? path.basename(parsed.dir) || 'index' : baseName;
17
+ return toPascalCase(nameToConvert);
18
+ }
19
+ function toPascalCase(str) {
20
+ if (str.includes('-')) {
21
+ return str
22
+ .split('-')
23
+ .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
24
+ .join('');
25
+ }
26
+ return str.charAt(0).toUpperCase() + str.slice(1);
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { MLASTDocument } from '@markuplint/ml-ast';
2
+ /**
3
+ * Detects whether a parsed component template contains slot usage.
4
+ *
5
+ * Supports:
6
+ * - Vue: `<slot>` element (`nodeName === 'slot'`)
7
+ * - Svelte 4: `<slot>` element (parsed as psblock `#ps:SlotElement`)
8
+ * - Svelte 5: `{@render children()}` (parsed as psblock `#ps:RenderTag`)
9
+ * - Astro: `<slot />` element (`nodeName === 'slot'`)
10
+ *
11
+ * @param doc - The parsed MLAST document to search
12
+ * @returns `true` if the template contains any slot or children render usage
13
+ */
14
+ export declare function detectSlots(doc: MLASTDocument): boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Detects whether a parsed component template contains slot usage.
3
+ *
4
+ * Supports:
5
+ * - Vue: `<slot>` element (`nodeName === 'slot'`)
6
+ * - Svelte 4: `<slot>` element (parsed as psblock `#ps:SlotElement`)
7
+ * - Svelte 5: `{@render children()}` (parsed as psblock `#ps:RenderTag`)
8
+ * - Astro: `<slot />` element (`nodeName === 'slot'`)
9
+ *
10
+ * @param doc - The parsed MLAST document to search
11
+ * @returns `true` if the template contains any slot or children render usage
12
+ */
13
+ export function detectSlots(doc) {
14
+ for (const node of doc.nodeList) {
15
+ if (node.type === 'starttag' && node.nodeName === 'slot') {
16
+ return true;
17
+ }
18
+ if (node.type === 'psblock' && (node.nodeName === '#ps:SlotElement' || node.nodeName === '#ps:RenderTag')) {
19
+ return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
@@ -0,0 +1,13 @@
1
+ import type { MLASTElement } from '@markuplint/ml-ast';
2
+ import type { PretenderAttr } from '@markuplint/ml-config';
3
+ /**
4
+ * Extracts static attributes from an MLAST element node as PretenderAttr entries.
5
+ *
6
+ * Only includes attributes with type `'attr'` (regular HTML attributes).
7
+ * Spread attributes are skipped. Boolean attributes (no value) are included
8
+ * without a `value` property.
9
+ *
10
+ * @param element - The MLAST element node to extract attributes from
11
+ * @returns Static attributes suitable for pretender definitions
12
+ */
13
+ export declare function extractAttrs(element: MLASTElement): readonly PretenderAttr[];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Extracts static attributes from an MLAST element node as PretenderAttr entries.
3
+ *
4
+ * Only includes attributes with type `'attr'` (regular HTML attributes).
5
+ * Spread attributes are skipped. Boolean attributes (no value) are included
6
+ * without a `value` property.
7
+ *
8
+ * @param element - The MLAST element node to extract attributes from
9
+ * @returns Static attributes suitable for pretender definitions
10
+ */
11
+ export function extractAttrs(element) {
12
+ const result = [];
13
+ for (const attr of element.attributes) {
14
+ if (attr.type !== 'attr') {
15
+ continue;
16
+ }
17
+ const value = attr.value.raw;
18
+ if (value === '') {
19
+ result.push({ name: attr.nodeName });
20
+ }
21
+ else {
22
+ result.push({ name: attr.nodeName, value });
23
+ }
24
+ }
25
+ return result;
26
+ }
@@ -0,0 +1,11 @@
1
+ import type { MLASTDocument, MLASTElement } from '@markuplint/ml-ast';
2
+ /**
3
+ * Extracts the first substantive element at depth=0 from a parsed document.
4
+ *
5
+ * Skips text nodes, comments, preprocessor-specific blocks (frontmatter, etc.),
6
+ * and end tags. Returns the first `starttag` node found at depth 0.
7
+ *
8
+ * @param doc - The parsed MLAST document to search
9
+ * @returns The root element, or `null` if only text/comments exist (Fragment-like).
10
+ */
11
+ export declare function extractRoot(doc: MLASTDocument): MLASTElement | null;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Extracts the first substantive element at depth=0 from a parsed document.
3
+ *
4
+ * Skips text nodes, comments, preprocessor-specific blocks (frontmatter, etc.),
5
+ * and end tags. Returns the first `starttag` node found at depth 0.
6
+ *
7
+ * @param doc - The parsed MLAST document to search
8
+ * @returns The root element, or `null` if only text/comments exist (Fragment-like).
9
+ */
10
+ export function extractRoot(doc) {
11
+ for (const node of doc.nodeList) {
12
+ if (node.type === 'starttag' && node.depth === 0 && !node.isFragment) {
13
+ return node;
14
+ }
15
+ }
16
+ return null;
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { PretenderScanTemplateOptions } from './types.js';
2
+ /**
3
+ * Template scanner for Vue, Svelte, and Astro component files.
4
+ *
5
+ * Delegates to each parser package's component-scanner subpath export
6
+ * via dynamic import, keeping framework-specific scanning logic co-located
7
+ * with the parser that understands the framework best.
8
+ *
9
+ * @param files - Absolute file paths to scan (relative paths cause a `ReferenceError`)
10
+ * @param options - Template scanner configuration (cwd, component names to ignore)
11
+ * @returns Discovered pretender mappings for all components found in the given files
12
+ */
13
+ export declare const templateScanner: (files: readonly string[], options?: PretenderScanTemplateOptions | undefined) => Promise<import("@markuplint/ml-config").Pretender[]>;
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createScanner } from '../create-scanner.js';
4
+ import { PretenderDirector } from '../pretender-director.js';
5
+ import { getScanner } from '../scanner-loader.js';
6
+ import { deriveName } from './derive-name.js';
7
+ /**
8
+ * Template scanner for Vue, Svelte, and Astro component files.
9
+ *
10
+ * Delegates to each parser package's component-scanner subpath export
11
+ * via dynamic import, keeping framework-specific scanning logic co-located
12
+ * with the parser that understands the framework best.
13
+ *
14
+ * @param files - Absolute file paths to scan (relative paths cause a `ReferenceError`)
15
+ * @param options - Template scanner configuration (cwd, component names to ignore)
16
+ * @returns Discovered pretender mappings for all components found in the given files
17
+ */
18
+ export const templateScanner = createScanner(async (files, options) => {
19
+ const cwd = options?.cwd ?? process.cwd();
20
+ const ignoreComponentNames = options?.ignoreComponentNames ?? [];
21
+ const director = new PretenderDirector();
22
+ for (const filePath of files) {
23
+ const componentName = deriveName(filePath);
24
+ if (ignoreComponentNames.includes(componentName)) {
25
+ continue;
26
+ }
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ const scanner = await getScanner(ext);
29
+ if (!scanner) {
30
+ continue;
31
+ }
32
+ let sourceCode;
33
+ try {
34
+ sourceCode = fs.readFileSync(filePath, 'utf8');
35
+ }
36
+ catch (error) {
37
+ // eslint-disable-next-line no-console
38
+ console.warn(`Failed to read component file: ${filePath}`, error instanceof Error ? error.message : error);
39
+ continue;
40
+ }
41
+ const scan = scanner.scanComponent(sourceCode);
42
+ if (!scan?.rootElement) {
43
+ continue;
44
+ }
45
+ const relFilePath = path.relative(cwd, filePath);
46
+ const attrs = scan.attrs.map(a => a.value === undefined ? { name: a.name } : { name: a.name, value: a.value });
47
+ const identity = attrs.length > 0 || scan.hasSlots
48
+ ? {
49
+ element: scan.rootElement,
50
+ ...(attrs.length > 0 ? { attrs } : {}),
51
+ slots: scan.hasSlots ? true : null,
52
+ }
53
+ : scan.rootElement;
54
+ director.add(componentName, identity, relFilePath, scan.line ?? 1, scan.col ?? 1, relFilePath);
55
+ }
56
+ return director.getPretenders();
57
+ });
@@ -0,0 +1,22 @@
1
+ import type { MLASTDocument } from '@markuplint/ml-ast';
2
+ type FrameworkType = 'vue' | 'svelte' | 'astro';
3
+ /**
4
+ * Determines the framework type from the file extension.
5
+ *
6
+ * @param filePath - The file path to check
7
+ * @returns The framework type, or `null` if the extension is not recognized
8
+ */
9
+ export declare function getFrameworkType(filePath: string): FrameworkType | null;
10
+ /**
11
+ * Checks if an error is a Node.js ERR_MODULE_NOT_FOUND error.
12
+ */
13
+ export declare function isModuleNotFoundError(error: unknown): boolean;
14
+ /**
15
+ * Parses a component file into an MLASTDocument using the appropriate framework parser.
16
+ *
17
+ * @param filePath - The absolute path to the component file
18
+ * @returns The parsed document, or `null` if the file extension is not supported
19
+ * or the required parser package is not installed.
20
+ */
21
+ export declare function parseComponent(filePath: string): Promise<MLASTDocument | null>;
22
+ export {};
@@ -0,0 +1,74 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const EXTENSION_MAP = {
4
+ '.vue': 'vue',
5
+ '.svelte': 'svelte',
6
+ '.astro': 'astro',
7
+ };
8
+ /**
9
+ * Determines the framework type from the file extension.
10
+ *
11
+ * @param filePath - The file path to check
12
+ * @returns The framework type, or `null` if the extension is not recognized
13
+ */
14
+ export function getFrameworkType(filePath) {
15
+ const ext = path.extname(filePath).toLowerCase();
16
+ return EXTENSION_MAP[ext] ?? null;
17
+ }
18
+ const PARSER_PACKAGES = {
19
+ vue: '@markuplint/vue-parser',
20
+ svelte: '@markuplint/svelte-parser',
21
+ astro: '@markuplint/astro-parser',
22
+ };
23
+ /**
24
+ * Checks if an error is a Node.js ERR_MODULE_NOT_FOUND error.
25
+ */
26
+ export function isModuleNotFoundError(error) {
27
+ return (error instanceof Error && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND');
28
+ }
29
+ /**
30
+ * Dynamically imports the appropriate parser for the given framework.
31
+ *
32
+ * @returns The parser, or `null` if the parser package is not installed.
33
+ */
34
+ async function getParser(framework) {
35
+ const pkg = PARSER_PACKAGES[framework];
36
+ try {
37
+ const mod = await import(pkg);
38
+ return mod.parser;
39
+ }
40
+ catch (error) {
41
+ if (isModuleNotFoundError(error)) {
42
+ // eslint-disable-next-line no-console
43
+ console.warn(`Parser package "${pkg}" is not installed. Skipping ${framework} files.`);
44
+ return null;
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Parses a component file into an MLASTDocument using the appropriate framework parser.
51
+ *
52
+ * @param filePath - The absolute path to the component file
53
+ * @returns The parsed document, or `null` if the file extension is not supported
54
+ * or the required parser package is not installed.
55
+ */
56
+ export async function parseComponent(filePath) {
57
+ const framework = getFrameworkType(filePath);
58
+ if (!framework) {
59
+ return null;
60
+ }
61
+ const parser = await getParser(framework);
62
+ if (!parser) {
63
+ return null;
64
+ }
65
+ try {
66
+ const sourceCode = fs.readFileSync(filePath, 'utf8');
67
+ return parser.parse(sourceCode);
68
+ }
69
+ catch (error) {
70
+ // eslint-disable-next-line no-console
71
+ console.warn(`Failed to parse component: ${filePath}`, error instanceof Error ? error.message : error);
72
+ return null;
73
+ }
74
+ }
@@ -0,0 +1,6 @@
1
+ import type { PretenderScanOptions } from '@markuplint/ml-config';
2
+ /**
3
+ * Options for the template scanner.
4
+ */
5
+ export interface PretenderScanTemplateOptions extends PretenderScanOptions {
6
+ }
@@ -0,0 +1 @@
1
+ export {};
package/lib/types.d.ts CHANGED
@@ -23,6 +23,13 @@ export type Identifier = Pretender['selector'];
23
23
  * Derived from the `as` property of a Pretender.
24
24
  */
25
25
  export type Identity = Pretender['as'];
26
+ /**
27
+ * An import path string used to uniquely identify a component across files.
28
+ * When provided, this is used as the map key instead of the component name,
29
+ * enabling disambiguation of identically named components from different locations
30
+ * (e.g., `../A/Button.vue` vs `../B/Button.vue`).
31
+ */
32
+ export type ImportPath = string;
26
33
  /**
27
34
  * Represents an attribute found on a JSX element during scanning.
28
35
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/pretenders",
3
- "version": "5.0.0-dev.5+e96392f56",
3
+ "version": "5.0.0-rc.1",
4
4
  "description": "It loads components and then creates the pretenders data from them.",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
@@ -28,11 +28,18 @@
28
28
  "clean": "tsc --build --clean tsconfig.build.json"
29
29
  },
30
30
  "dependencies": {
31
- "@markuplint/ml-config": "5.0.0-dev.5+e96392f56",
32
- "@markuplint/parser-utils": "5.0.0-dev.5+e96392f56",
31
+ "@markuplint/ml-ast": "5.0.0-rc.1",
32
+ "@markuplint/ml-config": "5.0.0-rc.1",
33
+ "@markuplint/parser-utils": "5.0.0-rc.1",
34
+ "es-module-lexer": "2.0.0",
33
35
  "glob": "13.0.6",
34
36
  "meow": "14.1.0",
35
- "typescript": "5.9.3"
37
+ "typescript": "6.0.2"
36
38
  },
37
- "gitHead": "e96392f56e4bc8165ba59622b41c822703a96372"
39
+ "optionalDependencies": {
40
+ "@markuplint/astro-parser": "5.0.0-rc.1",
41
+ "@markuplint/svelte-parser": "5.0.0-rc.1",
42
+ "@markuplint/vue-parser": "5.0.0-rc.1"
43
+ },
44
+ "gitHead": "0d6b4324d9a7d6b9e1ba57d4a57e45d36975cba9"
38
45
  }