@markuplint/pretenders 5.0.0-rc.0 → 5.0.0-rc.2

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,14 @@
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.2](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.1...v5.0.0-rc.2) (2026-04-15)
7
+
8
+ **Note:** Version bump only for package @markuplint/pretenders
9
+
10
+ # [5.0.0-rc.1](https://github.com/markuplint/markuplint/compare/v5.0.0-rc.0...v5.0.0-rc.1) (2026-03-27)
11
+
12
+ **Note:** Version bump only for package @markuplint/pretenders
13
+
6
14
  # [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
15
 
8
16
  ### Bug Fixes
package/README.md CHANGED
@@ -93,7 +93,7 @@ The JSX scanner detects **slots** (children). If a component accepts `children`
93
93
 
94
94
  ### Template Scanner
95
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.
96
+ The template scanner delegates to each parser package's `component-scanner` subpath export (e.g., `@markuplint/vue-parser/component-scanner`). Each parser's component-scanner uses its own MLAST parser to extract the root element at depth=0, detect static attributes, slot/children usage, and extract script source blocks. This keeps framework-specific scanning logic co-located with the parser that understands the framework best.
97
97
 
98
98
  ```vue
99
99
  <template>
@@ -123,13 +123,13 @@ Slot detection covers:
123
123
 
124
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
125
 
126
- Supported script block types:
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:
127
127
 
128
- - Vue `<script setup>` (all static imports are exposed as bindings)
128
+ - Vue `<script setup>` (via `@markuplint/vue-parser/component-scanner`)
129
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
130
+ - Svelte `<script>` (via `@markuplint/svelte-parser/component-scanner`; prefers instance script over module script)
131
+ - Astro frontmatter (via `@markuplint/astro-parser/component-scanner`)
132
+ - MDX top-level ESM (built-in)
133
133
 
134
134
  Dynamic imports with string literal specifiers (`import('./path')`) are included in bindings with `type: 'dynamic'`. Template literal and variable specifiers are excluded.
135
135
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @module component-scanner
3
+ *
4
+ * Type definitions for the Companion Module pattern.
5
+ * Each framework parser package provides its own `component-scanner` subpath
6
+ * implementing these interfaces. The pretenders package owns the types;
7
+ * parser packages import them for implementation.
8
+ */
9
+ /**
10
+ * Result of scanning a single component file for its root element information.
11
+ */
12
+ export interface ComponentScanResult {
13
+ /** The root element tag name, or `null` if the component is fragment-like */
14
+ readonly rootElement: string | null;
15
+ /** Static attributes on the root element */
16
+ readonly attrs: readonly ComponentScanAttr[];
17
+ /** Whether the component template contains slot usage (Vue `<slot>`, Svelte `{@render}`, etc.) */
18
+ readonly hasSlots: boolean;
19
+ /** Extracted script/ESM source block for import analysis */
20
+ readonly scriptSource?: ComponentScanScriptSource;
21
+ /** SVG namespace indicator (only set when root is in SVG namespace) */
22
+ readonly namespace?: 'svg';
23
+ /** Line number of the root element in the source */
24
+ readonly line?: number;
25
+ /** Column number of the root element in the source */
26
+ readonly col?: number;
27
+ }
28
+ /**
29
+ * A static attribute extracted from a component's root element.
30
+ */
31
+ export interface ComponentScanAttr {
32
+ /** The attribute name */
33
+ readonly name: string;
34
+ /** The attribute value (omitted for boolean attributes) */
35
+ readonly value?: string;
36
+ }
37
+ /**
38
+ * A script/ESM source block extracted from a component file.
39
+ * Used by import-resolver to analyze component imports.
40
+ */
41
+ export interface ComponentScanScriptSource {
42
+ /** The raw script content without delimiters */
43
+ readonly content: string;
44
+ /** The character offset of the content start within the original source */
45
+ readonly offset: number;
46
+ }
47
+ /**
48
+ * Interface for framework-specific component scanners.
49
+ * Implemented by each parser package's `component-scanner` subpath export.
50
+ */
51
+ export interface ComponentScanner {
52
+ /**
53
+ * Scans a single component source file and extracts root element information.
54
+ *
55
+ * @param sourceCode - The full source text of the component file
56
+ * @returns The scan result, or `null` if scanning fails or the file has no root element
57
+ */
58
+ scanComponent(sourceCode: string): ComponentScanResult | null;
59
+ /**
60
+ * Extracts the script/ESM source block from a component file.
61
+ * Optional — only needed for frameworks that embed scripts (Vue, Svelte, Astro).
62
+ *
63
+ * @param sourceCode - The full source text of the component file
64
+ * @returns The extracted script block, or `null` if none found
65
+ */
66
+ extractScriptSource?(sourceCode: string): ComponentScanScriptSource | null;
67
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module component-scanner
3
+ *
4
+ * Type definitions for the Companion Module pattern.
5
+ * Each framework parser package provides its own `component-scanner` subpath
6
+ * implementing these interfaces. The pretenders package owns the types;
7
+ * parser packages import them for implementation.
8
+ */
9
+ export {};
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * @module extract-script-source
3
3
  *
4
- * Framework-specific extractors that pull ESM source text from component files.
5
- * Each framework stores imports in different locations:
4
+ * Script source extractors for component files.
6
5
  *
7
- * | Framework | Source Block | Extraction Method |
8
- * |-----------|---------------------------|----------------------------|
9
- * | Vue | `<script setup>` tag | Regex on raw source |
10
- * | Svelte | `<script>` tag | Regex on raw source |
11
- * | Astro | Frontmatter (`---...---`) | Regex on raw source |
12
- * | MDX | Top-level ESM | Whole source (lexer-safe) |
6
+ * Vue `<script setup>`, Svelte `<script>`, and Astro frontmatter extraction
7
+ * has been moved to each parser's `component-scanner` subpath export.
8
+ * This module retains only:
9
+ *
10
+ * | Function | Purpose |
11
+ * |----------------------------------|------------------------------------------------------|
12
+ * | `extractVueScript` | Vue Options API `<script>` block (non-setup) |
13
+ * | `extractVueOptionsApiComponents` | Vue `components: { ... }` registration extraction |
14
+ * | `extractMdxEsm` | MDX top-level ESM (no parser package for MDX) |
13
15
  */
14
16
  /**
15
17
  * The result of extracting a script source block from a component file.
@@ -20,15 +22,6 @@ export interface ScriptSourceBlock {
20
22
  /** The offset (in characters) of the content start within the original source */
21
23
  readonly offset: number;
22
24
  }
23
- /**
24
- * Extracts the content of the `<script setup>` block from a Vue SFC source.
25
- * Handles optional `lang` attribute (e.g., `<script setup lang="ts">`).
26
- * Only matches `<script setup>` — regular `<script>` blocks are ignored.
27
- *
28
- * @param source - The full Vue SFC source text
29
- * @returns The extracted script block, or `null` if no `<script setup>` is found
30
- */
31
- export declare function extractVueScriptSetup(source: string): ScriptSourceBlock | null;
32
25
  /**
33
26
  * Extracts the content of a regular `<script>` block (NOT `<script setup>`) from a Vue SFC source.
34
27
  * Only matches `<script>` without the `setup` attribute.
@@ -46,22 +39,6 @@ export declare function extractVueScript(source: string): ScriptSourceBlock | nu
46
39
  * @returns An array of component local names referenced in the `components` registration
47
40
  */
48
41
  export declare function extractVueOptionsApiComponents(scriptContent: string): string[];
49
- /**
50
- * Extracts the content of the `<script>` block from a Svelte component source.
51
- * Prefers the instance `<script>` over `<script context="module">`.
52
- * Falls back to the module script if no instance script is found.
53
- *
54
- * @param source - The full Svelte component source text
55
- * @returns The extracted script block, or `null` if no `<script>` is found
56
- */
57
- export declare function extractSvelteScript(source: string): ScriptSourceBlock | null;
58
- /**
59
- * Extracts the content of the frontmatter block (`---...---`) from an Astro component source.
60
- *
61
- * @param source - The full Astro component source text
62
- * @returns The extracted frontmatter block, or `null` if no frontmatter is found
63
- */
64
- export declare function extractAstroFrontmatter(source: string): ScriptSourceBlock | null;
65
42
  /**
66
43
  * Extracts the top-level ESM block from an MDX file.
67
44
  * MDX files have standard ESM import/export statements at the top of the file,
@@ -1,44 +1,18 @@
1
1
  /**
2
2
  * @module extract-script-source
3
3
  *
4
- * Framework-specific extractors that pull ESM source text from component files.
5
- * Each framework stores imports in different locations:
4
+ * Script source extractors for component files.
6
5
  *
7
- * | Framework | Source Block | Extraction Method |
8
- * |-----------|---------------------------|----------------------------|
9
- * | Vue | `<script setup>` tag | Regex on raw source |
10
- * | Svelte | `<script>` tag | Regex on raw source |
11
- * | Astro | Frontmatter (`---...---`) | Regex on raw source |
12
- * | MDX | Top-level ESM | Whole source (lexer-safe) |
13
- */
14
- /**
15
- * Extracts the content of the `<script setup>` block from a Vue SFC source.
16
- * Handles optional `lang` attribute (e.g., `<script setup lang="ts">`).
17
- * Only matches `<script setup>` — regular `<script>` blocks are ignored.
6
+ * Vue `<script setup>`, Svelte `<script>`, and Astro frontmatter extraction
7
+ * has been moved to each parser's `component-scanner` subpath export.
8
+ * This module retains only:
18
9
  *
19
- * @param source - The full Vue SFC source text
20
- * @returns The extracted script block, or `null` if no `<script setup>` is found
10
+ * | Function | Purpose |
11
+ * |----------------------------------|------------------------------------------------------|
12
+ * | `extractVueScript` | Vue Options API `<script>` block (non-setup) |
13
+ * | `extractVueOptionsApiComponents` | Vue `components: { ... }` registration extraction |
14
+ * | `extractMdxEsm` | MDX top-level ESM (no parser package for MDX) |
21
15
  */
22
- export function extractVueScriptSetup(source) {
23
- // Match <script setup> with optional attributes like lang="ts"
24
- const re = /<script\s[^>]*?\bsetup\b[^>]*>/i;
25
- const match = re.exec(source);
26
- if (!match) {
27
- return null;
28
- }
29
- const startTag = match[0];
30
- const contentStart = match.index + startTag.length;
31
- const endTagRe = /<\/script\s*>/i;
32
- const remaining = source.slice(contentStart);
33
- const endMatch = endTagRe.exec(remaining);
34
- if (!endMatch) {
35
- return null;
36
- }
37
- return {
38
- content: remaining.slice(0, endMatch.index),
39
- offset: contentStart,
40
- };
41
- }
42
16
  /**
43
17
  * Extracts the content of a regular `<script>` block (NOT `<script setup>`) from a Vue SFC source.
44
18
  * Only matches `<script>` without the `setup` attribute.
@@ -108,64 +82,6 @@ export function extractVueOptionsApiComponents(scriptContent) {
108
82
  }
109
83
  return names;
110
84
  }
111
- /**
112
- * Extracts the content of the `<script>` block from a Svelte component source.
113
- * Prefers the instance `<script>` over `<script context="module">`.
114
- * Falls back to the module script if no instance script is found.
115
- *
116
- * @param source - The full Svelte component source text
117
- * @returns The extracted script block, or `null` if no `<script>` is found
118
- */
119
- export function extractSvelteScript(source) {
120
- const re = /<script(?:\s[^>]*)?>/gi;
121
- let match;
122
- let moduleBlock = null;
123
- while ((match = re.exec(source)) !== null) {
124
- const startTag = match[0];
125
- const isModule = /\bcontext\s*=\s*["']module["']/i.test(startTag);
126
- const contentStart = match.index + startTag.length;
127
- const endTagRe = /<\/script\s*>/i;
128
- const remaining = source.slice(contentStart);
129
- const endMatch = endTagRe.exec(remaining);
130
- if (!endMatch) {
131
- continue;
132
- }
133
- const block = {
134
- content: remaining.slice(0, endMatch.index),
135
- offset: contentStart,
136
- };
137
- if (!isModule) {
138
- return block; // Prefer instance script
139
- }
140
- // Remember module script as fallback
141
- moduleBlock ??= block;
142
- }
143
- return moduleBlock;
144
- }
145
- /**
146
- * Extracts the content of the frontmatter block (`---...---`) from an Astro component source.
147
- *
148
- * @param source - The full Astro component source text
149
- * @returns The extracted frontmatter block, or `null` if no frontmatter is found
150
- */
151
- export function extractAstroFrontmatter(source) {
152
- const re = /^(?:\s*\n)?---\r?\n/;
153
- const startMatch = re.exec(source);
154
- if (!startMatch) {
155
- return null;
156
- }
157
- const contentStart = startMatch[0].length;
158
- const afterStart = source.slice(contentStart);
159
- const endRe = /\r?\n---\r?\n/;
160
- const endMatch = endRe.exec(afterStart);
161
- if (!endMatch) {
162
- return null;
163
- }
164
- return {
165
- content: afterStart.slice(0, endMatch.index),
166
- offset: contentStart,
167
- };
168
- }
169
85
  /**
170
86
  * Extracts the top-level ESM block from an MDX file.
171
87
  * MDX files have standard ESM import/export statements at the top of the file,
@@ -10,12 +10,11 @@
10
10
  *
11
11
  * ## Supported frameworks
12
12
  *
13
- * - Vue `<script setup>` (all static imports are exposed as bindings)
14
- * - Vue Options API `components` property (fallback when no `<script setup>`;
15
- * only imports registered in `components: { ... }` are returned)
16
- * - Svelte `<script>` tags (prefers instance script over module script)
17
- * - Astro frontmatter (`---...---`)
18
- * - MDX top-level ESM
13
+ * - Vue `<script setup>` (via `@markuplint/vue-parser/component-scanner`)
14
+ * - Vue Options API `components` property (fallback; uses built-in regex extraction)
15
+ * - Svelte `<script>` tags (via `@markuplint/svelte-parser/component-scanner`)
16
+ * - Astro frontmatter (via `@markuplint/astro-parser/component-scanner`)
17
+ * - MDX top-level ESM (built-in extraction — no parser package)
19
18
  *
20
19
  * ## Dynamic imports
21
20
  *
@@ -29,43 +28,14 @@
29
28
  * `resolveBarrelExport` is a standalone utility (not called by `analyzeImports`)
30
29
  * that resolves a named import from a barrel directory (e.g., `'./components'`)
31
30
  * to its original source module. Only single-level re-exports are resolved.
32
- *
33
- * ### Usage example: resolving barrel imports
34
- *
35
- * ```ts
36
- * import { analyzeImports, resolveComponentImport, resolveBarrelExport } from '@markuplint/pretenders';
37
- *
38
- * const result = await analyzeImports('App.vue', source);
39
- * const binding = resolveComponentImport('Button', result.bindings);
40
- *
41
- * if (binding) {
42
- * // Check if the import source is a barrel directory
43
- * const originalSource = resolveBarrelExport(binding.source, binding.importedName, filePath);
44
- * // originalSource: './Button.vue' (resolved from './components' barrel)
45
- * }
46
- * ```
47
- *
48
- * ## Excludes
49
- *
50
- * - `import.meta` references
51
- * - Dynamic imports with template literals or variable specifiers
52
- * - Multi-level barrel chains (only single-level re-exports are resolved)
53
- *
54
- * ## Future integration
55
- *
56
- * The current implementation uses regex-based source extraction.
57
- * When the CLI multi-framework dispatch (#3340) creates a unified parsing
58
- * pipeline, MLAST psblock-based extraction can be integrated by parsing
59
- * the file once and passing the document to both templateScanner and
60
- * import-resolver.
61
31
  */
62
32
  import type { ImportBinding, ImportAnalysisResult } from './types.js';
63
33
  export { resolveBarrelExport } from './resolve-barrel.js';
64
34
  export type { ImportBinding, ImportAnalysisResult } from './types.js';
65
35
  /**
66
36
  * Analyzes a component file's source text and extracts all static import bindings.
67
- * Automatically detects the framework type from the file extension and extracts
68
- * the appropriate source block (script setup, script, frontmatter, or top-level ESM).
37
+ * Automatically detects the framework type from the file extension and delegates
38
+ * script source extraction to the appropriate component scanner.
69
39
  *
70
40
  * @param filePath - The absolute or relative file path (used for framework detection)
71
41
  * @param source - The full source text of the component file
@@ -10,12 +10,11 @@
10
10
  *
11
11
  * ## Supported frameworks
12
12
  *
13
- * - Vue `<script setup>` (all static imports are exposed as bindings)
14
- * - Vue Options API `components` property (fallback when no `<script setup>`;
15
- * only imports registered in `components: { ... }` are returned)
16
- * - Svelte `<script>` tags (prefers instance script over module script)
17
- * - Astro frontmatter (`---...---`)
18
- * - MDX top-level ESM
13
+ * - Vue `<script setup>` (via `@markuplint/vue-parser/component-scanner`)
14
+ * - Vue Options API `components` property (fallback; uses built-in regex extraction)
15
+ * - Svelte `<script>` tags (via `@markuplint/svelte-parser/component-scanner`)
16
+ * - Astro frontmatter (via `@markuplint/astro-parser/component-scanner`)
17
+ * - MDX top-level ESM (built-in extraction — no parser package)
19
18
  *
20
19
  * ## Dynamic imports
21
20
  *
@@ -29,38 +28,10 @@
29
28
  * `resolveBarrelExport` is a standalone utility (not called by `analyzeImports`)
30
29
  * that resolves a named import from a barrel directory (e.g., `'./components'`)
31
30
  * to its original source module. Only single-level re-exports are resolved.
32
- *
33
- * ### Usage example: resolving barrel imports
34
- *
35
- * ```ts
36
- * import { analyzeImports, resolveComponentImport, resolveBarrelExport } from '@markuplint/pretenders';
37
- *
38
- * const result = await analyzeImports('App.vue', source);
39
- * const binding = resolveComponentImport('Button', result.bindings);
40
- *
41
- * if (binding) {
42
- * // Check if the import source is a barrel directory
43
- * const originalSource = resolveBarrelExport(binding.source, binding.importedName, filePath);
44
- * // originalSource: './Button.vue' (resolved from './components' barrel)
45
- * }
46
- * ```
47
- *
48
- * ## Excludes
49
- *
50
- * - `import.meta` references
51
- * - Dynamic imports with template literals or variable specifiers
52
- * - Multi-level barrel chains (only single-level re-exports are resolved)
53
- *
54
- * ## Future integration
55
- *
56
- * The current implementation uses regex-based source extraction.
57
- * When the CLI multi-framework dispatch (#3340) creates a unified parsing
58
- * pipeline, MLAST psblock-based extraction can be integrated by parsing
59
- * the file once and passing the document to both templateScanner and
60
- * import-resolver.
61
31
  */
62
32
  import path from 'node:path';
63
- import { extractVueScriptSetup, extractVueScript, extractVueOptionsApiComponents, extractSvelteScript, extractAstroFrontmatter, extractMdxEsm, } from './extract-script-source.js';
33
+ import { getScanner } from '../scanner-loader.js';
34
+ import { extractVueScript, extractVueOptionsApiComponents, extractMdxEsm } from './extract-script-source.js';
64
35
  import { parseImports } from './parse-imports.js';
65
36
  export { resolveBarrelExport } from './resolve-barrel.js';
66
37
  const EXTENSION_MAP = {
@@ -71,16 +42,30 @@ const EXTENSION_MAP = {
71
42
  };
72
43
  /**
73
44
  * Determines the framework type from the file extension.
74
- * Local to import-resolver to include MDX without affecting templateScanner.
75
45
  */
76
46
  function getImportFrameworkType(filePath) {
77
47
  const ext = path.extname(filePath).toLowerCase();
78
48
  return EXTENSION_MAP[ext] ?? null;
79
49
  }
50
+ /**
51
+ * Extracts the script source from a component file using the component scanner
52
+ * if available, or falls back to built-in extraction for MDX.
53
+ */
54
+ async function extractScriptSource(filePath, source, framework) {
55
+ if (framework === 'mdx') {
56
+ return extractMdxEsm(source);
57
+ }
58
+ const ext = path.extname(filePath).toLowerCase();
59
+ const scanner = await getScanner(ext);
60
+ if (scanner?.extractScriptSource) {
61
+ return scanner.extractScriptSource(source);
62
+ }
63
+ return null;
64
+ }
80
65
  /**
81
66
  * Analyzes a component file's source text and extracts all static import bindings.
82
- * Automatically detects the framework type from the file extension and extracts
83
- * the appropriate source block (script setup, script, frontmatter, or top-level ESM).
67
+ * Automatically detects the framework type from the file extension and delegates
68
+ * script source extraction to the appropriate component scanner.
84
69
  *
85
70
  * @param filePath - The absolute or relative file path (used for framework detection)
86
71
  * @param source - The full source text of the component file
@@ -92,30 +77,17 @@ export async function analyzeImports(filePath, source) {
92
77
  if (!framework) {
93
78
  return null;
94
79
  }
95
- let scriptSource = null;
96
- switch (framework) {
97
- case 'vue': {
98
- scriptSource = extractVueScriptSetup(source);
99
- if (!scriptSource) {
100
- // Fallback to Vue Options API: extract regular <script>, parse imports,
101
- // then filter to only those registered in the `components` property.
102
- return analyzeVueOptionsApi(source);
103
- }
104
- break;
105
- }
106
- case 'svelte': {
107
- scriptSource = extractSvelteScript(source);
108
- break;
109
- }
110
- case 'astro': {
111
- scriptSource = extractAstroFrontmatter(source);
112
- break;
113
- }
114
- case 'mdx': {
115
- scriptSource = extractMdxEsm(source);
116
- break;
80
+ // Vue Options API requires special handling: extract regular <script>,
81
+ // parse imports, then filter to only those registered in `components: { ... }`
82
+ if (framework === 'vue') {
83
+ const scriptSource = await extractScriptSource(filePath, source, framework);
84
+ if (!scriptSource) {
85
+ return analyzeVueOptionsApi(source);
117
86
  }
87
+ const bindings = await parseImports(scriptSource.content);
88
+ return { bindings };
118
89
  }
90
+ const scriptSource = await extractScriptSource(filePath, source, framework);
119
91
  if (!scriptSource) {
120
92
  return { bindings: [] };
121
93
  }
@@ -168,10 +140,6 @@ export function resolveComponentImport(componentName, bindings) {
168
140
  }
169
141
  /**
170
142
  * Converts a kebab-case string to PascalCase.
171
- * Used for Vue's component name resolution where `<my-button>` maps to `MyButton`.
172
- *
173
- * @param str - The kebab-case string
174
- * @returns The PascalCase equivalent
175
143
  */
176
144
  function kebabToPascalCase(str) {
177
145
  if (!str.includes('-')) {
package/lib/index.d.ts CHANGED
@@ -32,3 +32,4 @@ export type { ScanOptions } from './scan.js';
32
32
  export { analyzeImports, resolveComponentImport, resolveBarrelExport } from './import-resolver/index.js';
33
33
  export type { ImportBinding, ImportAnalysisResult } from './import-resolver/types.js';
34
34
  export type * from './types.js';
35
+ export type { ComponentScanner, ComponentScanResult, ComponentScanAttr, ComponentScanScriptSource, } from './component-scanner.js';
@@ -32,5 +32,5 @@ export declare class PretenderDirector {
32
32
  *
33
33
  * @returns A sorted array of resolved Pretender objects
34
34
  */
35
- getPretenders(): import("packages/@markuplint/ml-config/lib/types.js").Pretender[];
35
+ getPretenders(): import("@markuplint/ml-config").Pretender[];
36
36
  }
package/lib/scan.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface ScanOptions {
11
11
  * runs both scanners in parallel, and merges + sorts the results.
12
12
  *
13
13
  * - `.js`, `.jsx`, `.ts`, `.tsx` → {@link jsxScanner}
14
- * - `.vue`, `.svelte`, `.astro` → {@link templateScanner}
14
+ * - `.vue`, `.svelte`, `.astro` → {@link templateScanner} (delegates to parser component-scanners)
15
15
  *
16
16
  * @param files - Absolute file paths to scan
17
17
  * @param options - Optional scan configuration
package/lib/scan.js CHANGED
@@ -6,7 +6,7 @@ import { templateScanner } from './template/index.js';
6
6
  * runs both scanners in parallel, and merges + sorts the results.
7
7
  *
8
8
  * - `.js`, `.jsx`, `.ts`, `.tsx` → {@link jsxScanner}
9
- * - `.vue`, `.svelte`, `.astro` → {@link templateScanner}
9
+ * - `.vue`, `.svelte`, `.astro` → {@link templateScanner} (delegates to parser component-scanners)
10
10
  *
11
11
  * @param files - Absolute file paths to scan
12
12
  * @param options - Optional scan configuration
@@ -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
+ }
@@ -2,9 +2,9 @@ import type { PretenderScanTemplateOptions } from './types.js';
2
2
  /**
3
3
  * Template scanner for Vue, Svelte, and Astro component files.
4
4
  *
5
- * Parses component files using markuplint's existing framework parsers,
6
- * extracts root elements at depth=0, detects static attributes and slot usage,
7
- * and registers component-to-element mappings via PretenderDirector.
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
8
  *
9
9
  * @param files - Absolute file paths to scan (relative paths cause a `ReferenceError`)
10
10
  * @param options - Template scanner configuration (cwd, component names to ignore)
@@ -1,17 +1,15 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import { createScanner } from '../create-scanner.js';
3
4
  import { PretenderDirector } from '../pretender-director.js';
5
+ import { getScanner } from '../scanner-loader.js';
4
6
  import { deriveName } from './derive-name.js';
5
- import { detectSlots } from './detect-slots.js';
6
- import { extractAttrs } from './extract-attrs.js';
7
- import { extractRoot } from './extract-root.js';
8
- import { parseComponent } from './parse-component.js';
9
7
  /**
10
8
  * Template scanner for Vue, Svelte, and Astro component files.
11
9
  *
12
- * Parses component files using markuplint's existing framework parsers,
13
- * extracts root elements at depth=0, detects static attributes and slot usage,
14
- * and registers component-to-element mappings via PretenderDirector.
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.
15
13
  *
16
14
  * @param files - Absolute file paths to scan (relative paths cause a `ReferenceError`)
17
15
  * @param options - Template scanner configuration (cwd, component names to ignore)
@@ -26,26 +24,34 @@ export const templateScanner = createScanner(async (files, options) => {
26
24
  if (ignoreComponentNames.includes(componentName)) {
27
25
  continue;
28
26
  }
29
- const doc = await parseComponent(filePath);
30
- if (!doc) {
27
+ const ext = path.extname(filePath).toLowerCase();
28
+ const scanner = await getScanner(ext);
29
+ if (!scanner) {
31
30
  continue;
32
31
  }
33
- const root = extractRoot(doc);
34
- if (!root) {
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) {
35
43
  continue;
36
44
  }
37
- const tagName = root.nodeName;
38
- const attrs = extractAttrs(root);
39
- const hasSlots = detectSlots(doc);
40
45
  const relFilePath = path.relative(cwd, filePath);
41
- const identity = attrs.length > 0 || hasSlots
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
42
48
  ? {
43
- element: tagName,
49
+ element: scan.rootElement,
44
50
  ...(attrs.length > 0 ? { attrs } : {}),
45
- slots: hasSlots ? true : null,
51
+ slots: scan.hasSlots ? true : null,
46
52
  }
47
- : tagName;
48
- director.add(componentName, identity, relFilePath, root.line, root.col, relFilePath);
53
+ : scan.rootElement;
54
+ director.add(componentName, identity, relFilePath, scan.line ?? 1, scan.col ?? 1, relFilePath);
49
55
  }
50
56
  return director.getPretenders();
51
57
  });
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@markuplint/pretenders",
3
- "version": "5.0.0-rc.0",
3
+ "version": "5.0.0-rc.2",
4
4
  "description": "It loads components and then creates the pretenders data from them.",
5
- "repository": "git@github.com:markuplint/markuplint.git",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/markuplint/markuplint.git",
8
+ "directory": "packages/@markuplint/pretenders"
9
+ },
6
10
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
11
  "license": "MIT",
8
12
  "engines": {
@@ -28,18 +32,18 @@
28
32
  "clean": "tsc --build --clean tsconfig.build.json"
29
33
  },
30
34
  "dependencies": {
31
- "@markuplint/ml-ast": "5.0.0-rc.0",
32
- "@markuplint/ml-config": "5.0.0-rc.0",
33
- "@markuplint/parser-utils": "5.0.0-rc.0",
35
+ "@markuplint/ml-ast": "5.0.0-rc.2",
36
+ "@markuplint/ml-config": "5.0.0-rc.2",
37
+ "@markuplint/parser-utils": "5.0.0-rc.2",
34
38
  "es-module-lexer": "2.0.0",
35
39
  "glob": "13.0.6",
36
40
  "meow": "14.1.0",
37
- "typescript": "5.9.3"
41
+ "typescript": "6.0.2"
38
42
  },
39
43
  "optionalDependencies": {
40
- "@markuplint/astro-parser": "5.0.0-rc.0",
41
- "@markuplint/svelte-parser": "5.0.0-rc.0",
42
- "@markuplint/vue-parser": "5.0.0-rc.0"
44
+ "@markuplint/astro-parser": "5.0.0-rc.2",
45
+ "@markuplint/svelte-parser": "5.0.0-rc.2",
46
+ "@markuplint/vue-parser": "5.0.0-rc.2"
43
47
  },
44
- "gitHead": "ebf4d7cfca0c259aead3b292c6b8a202db4cd802"
48
+ "gitHead": "e43763858d9234c417053becc73dbd088c1e7ea6"
45
49
  }