@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
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @module extract-script-source
3
+ *
4
+ * Framework-specific extractors that pull ESM source text from component files.
5
+ * Each framework stores imports in different locations:
6
+ *
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
+ * The result of extracting a script source block from a component file.
16
+ */
17
+ export interface ScriptSourceBlock {
18
+ /** The raw script/ESM content without delimiters */
19
+ readonly content: string;
20
+ /** The offset (in characters) of the content start within the original source */
21
+ readonly offset: number;
22
+ }
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
+ /**
33
+ * Extracts the content of a regular `<script>` block (NOT `<script setup>`) from a Vue SFC source.
34
+ * Only matches `<script>` without the `setup` attribute.
35
+ *
36
+ * @param source - The full Vue SFC source text
37
+ * @returns The extracted script block, or `null` if no regular `<script>` is found
38
+ */
39
+ export declare function extractVueScript(source: string): ScriptSourceBlock | null;
40
+ /**
41
+ * Extracts component names registered in the Vue Options API `components` property.
42
+ * Handles both shorthand (`{ Button }`) and aliased (`{ Btn: MyButton }`) forms.
43
+ * For aliased forms, returns the value (the import name), not the key (the template name).
44
+ *
45
+ * @param scriptContent - The content of the `<script>` block (without tags)
46
+ * @returns An array of component local names referenced in the `components` registration
47
+ */
48
+ 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
+ /**
66
+ * Extracts the top-level ESM block from an MDX file.
67
+ * MDX files have standard ESM import/export statements at the top of the file,
68
+ * followed by markdown/JSX content. This function extracts only the contiguous
69
+ * block of import/export lines (including blank lines within the block),
70
+ * stopping at the first line that is clearly non-ESM content.
71
+ *
72
+ * Tracks brace depth to skip intermediate lines inside multi-line
73
+ * import/export blocks (e.g., `import { A, B } from '...'`).
74
+ * Note: the closing line of a multi-line block (e.g., `} from '...'`)
75
+ * is not recognized as ESM, so standalone multi-line imports are
76
+ * not captured. Single-line imports preceding a multi-line block
77
+ * are still returned correctly.
78
+ *
79
+ * @param source - The full MDX source text
80
+ * @returns The ESM block, or `null` if no import/export statements are found at the top
81
+ */
82
+ export declare function extractMdxEsm(source: string): ScriptSourceBlock | null;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * @module extract-script-source
3
+ *
4
+ * Framework-specific extractors that pull ESM source text from component files.
5
+ * Each framework stores imports in different locations:
6
+ *
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.
18
+ *
19
+ * @param source - The full Vue SFC source text
20
+ * @returns The extracted script block, or `null` if no `<script setup>` is found
21
+ */
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
+ /**
43
+ * Extracts the content of a regular `<script>` block (NOT `<script setup>`) from a Vue SFC source.
44
+ * Only matches `<script>` without the `setup` attribute.
45
+ *
46
+ * @param source - The full Vue SFC source text
47
+ * @returns The extracted script block, or `null` if no regular `<script>` is found
48
+ */
49
+ export function extractVueScript(source) {
50
+ const re = /<script(?:\s[^>]*)?>/gi;
51
+ let match;
52
+ while ((match = re.exec(source)) !== null) {
53
+ const startTag = match[0];
54
+ // Skip <script setup> blocks
55
+ if (/\bsetup\b/i.test(startTag)) {
56
+ continue;
57
+ }
58
+ const contentStart = match.index + startTag.length;
59
+ const remaining = source.slice(contentStart);
60
+ const endMatch = /<\/script\s*>/i.exec(remaining);
61
+ if (!endMatch) {
62
+ continue;
63
+ }
64
+ return {
65
+ content: remaining.slice(0, endMatch.index),
66
+ offset: contentStart,
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Extracts component names registered in the Vue Options API `components` property.
73
+ * Handles both shorthand (`{ Button }`) and aliased (`{ Btn: MyButton }`) forms.
74
+ * For aliased forms, returns the value (the import name), not the key (the template name).
75
+ *
76
+ * @param scriptContent - The content of the `<script>` block (without tags)
77
+ * @returns An array of component local names referenced in the `components` registration
78
+ */
79
+ export function extractVueOptionsApiComponents(scriptContent) {
80
+ if (!scriptContent) {
81
+ return [];
82
+ }
83
+ // Match `components: { ... }` allowing for multiline
84
+ const re = /\bcomponents\s*:\s*\{([^}]*)\}/;
85
+ const match = re.exec(scriptContent);
86
+ if (!match?.[1]) {
87
+ return [];
88
+ }
89
+ const names = [];
90
+ for (const entry of match[1].split(',')) {
91
+ const trimmed = entry.trim();
92
+ if (!trimmed) {
93
+ continue;
94
+ }
95
+ // Split on first colon only to handle values containing colons
96
+ const colonIdx = trimmed.indexOf(':');
97
+ if (colonIdx === -1) {
98
+ // Shorthand: `Button` → use as-is
99
+ names.push(trimmed);
100
+ }
101
+ else {
102
+ // Aliased form: `Btn: MyButton` → use value `MyButton`
103
+ const value = trimmed.slice(colonIdx + 1).trim();
104
+ if (value) {
105
+ names.push(value);
106
+ }
107
+ }
108
+ }
109
+ return names;
110
+ }
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
+ /**
170
+ * Extracts the top-level ESM block from an MDX file.
171
+ * MDX files have standard ESM import/export statements at the top of the file,
172
+ * followed by markdown/JSX content. This function extracts only the contiguous
173
+ * block of import/export lines (including blank lines within the block),
174
+ * stopping at the first line that is clearly non-ESM content.
175
+ *
176
+ * Tracks brace depth to skip intermediate lines inside multi-line
177
+ * import/export blocks (e.g., `import { A, B } from '...'`).
178
+ * Note: the closing line of a multi-line block (e.g., `} from '...'`)
179
+ * is not recognized as ESM, so standalone multi-line imports are
180
+ * not captured. Single-line imports preceding a multi-line block
181
+ * are still returned correctly.
182
+ *
183
+ * @param source - The full MDX source text
184
+ * @returns The ESM block, or `null` if no import/export statements are found at the top
185
+ */
186
+ export function extractMdxEsm(source) {
187
+ const lines = source.split('\n');
188
+ let esmEnd = 0;
189
+ let pos = 0;
190
+ let braceDepth = 0;
191
+ for (const line of lines) {
192
+ const trimmed = line.trim();
193
+ // Track brace depth for multi-line imports
194
+ for (const ch of line) {
195
+ if (ch === '{') {
196
+ braceDepth++;
197
+ }
198
+ if (ch === '}') {
199
+ braceDepth--;
200
+ }
201
+ }
202
+ pos += line.length + 1; // +1 for '\n'
203
+ // Inside a multi-line import/export block
204
+ if (braceDepth > 0) {
205
+ continue;
206
+ }
207
+ // ESM-like lines: import, export, empty, single-line comments
208
+ if (trimmed === '' || /^import\s/.test(trimmed) || /^export\s/.test(trimmed) || trimmed.startsWith('//')) {
209
+ esmEnd = pos;
210
+ continue;
211
+ }
212
+ // Non-ESM content (markdown, JSX, etc.) — stop
213
+ break;
214
+ }
215
+ if (esmEnd === 0) {
216
+ return null;
217
+ }
218
+ const content = source.slice(0, esmEnd);
219
+ // Verify the extracted block actually contains import/export statements
220
+ if (!/\b(?:import|export)\s/.test(content)) {
221
+ return null;
222
+ }
223
+ return {
224
+ content,
225
+ offset: 0,
226
+ };
227
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @module import-resolver
3
+ *
4
+ * Import analysis module that extracts import statements from `<script>` /
5
+ * frontmatter / ESM blocks in component files, linking template component
6
+ * usage to source file locations.
7
+ *
8
+ * Uses es-module-lexer (WASM-based) to identify import specifiers, then
9
+ * supplements with regex parsing on statement slices to extract local names.
10
+ *
11
+ * ## Supported frameworks
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
19
+ *
20
+ * ## Dynamic imports
21
+ *
22
+ * Dynamic imports with string literal specifiers (`import('./path')`) are
23
+ * included in the bindings with `type: 'dynamic'`. These bindings use
24
+ * `localName: '*'` as a sentinel since dynamic imports have no local binding.
25
+ * Template literal and variable specifiers are excluded.
26
+ *
27
+ * ## Barrel file resolution
28
+ *
29
+ * `resolveBarrelExport` is a standalone utility (not called by `analyzeImports`)
30
+ * that resolves a named import from a barrel directory (e.g., `'./components'`)
31
+ * 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
+ */
62
+ import type { ImportBinding, ImportAnalysisResult } from './types.js';
63
+ export { resolveBarrelExport } from './resolve-barrel.js';
64
+ export type { ImportBinding, ImportAnalysisResult } from './types.js';
65
+ /**
66
+ * 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).
69
+ *
70
+ * @param filePath - The absolute or relative file path (used for framework detection)
71
+ * @param source - The full source text of the component file
72
+ * @returns The analysis result with all import bindings, or `null` if the framework
73
+ * is not supported or no relevant script block is found
74
+ */
75
+ export declare function analyzeImports(filePath: string, source: string): Promise<ImportAnalysisResult | null>;
76
+ /**
77
+ * Resolves a component name used in a template to its import source path.
78
+ * For Vue, handles both PascalCase (`<MyButton>`) and kebab-case (`<my-button>`)
79
+ * representations of the same component by normalizing to PascalCase for lookup.
80
+ *
81
+ * @param componentName - The component name as used in the template
82
+ * @param bindings - The import bindings extracted from the script block
83
+ * @returns The matching import binding, or `undefined` if no match is found
84
+ */
85
+ export declare function resolveComponentImport(componentName: string, bindings: readonly ImportBinding[]): ImportBinding | undefined;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @module import-resolver
3
+ *
4
+ * Import analysis module that extracts import statements from `<script>` /
5
+ * frontmatter / ESM blocks in component files, linking template component
6
+ * usage to source file locations.
7
+ *
8
+ * Uses es-module-lexer (WASM-based) to identify import specifiers, then
9
+ * supplements with regex parsing on statement slices to extract local names.
10
+ *
11
+ * ## Supported frameworks
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
19
+ *
20
+ * ## Dynamic imports
21
+ *
22
+ * Dynamic imports with string literal specifiers (`import('./path')`) are
23
+ * included in the bindings with `type: 'dynamic'`. These bindings use
24
+ * `localName: '*'` as a sentinel since dynamic imports have no local binding.
25
+ * Template literal and variable specifiers are excluded.
26
+ *
27
+ * ## Barrel file resolution
28
+ *
29
+ * `resolveBarrelExport` is a standalone utility (not called by `analyzeImports`)
30
+ * that resolves a named import from a barrel directory (e.g., `'./components'`)
31
+ * 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
+ */
62
+ import path from 'node:path';
63
+ import { extractVueScriptSetup, extractVueScript, extractVueOptionsApiComponents, extractSvelteScript, extractAstroFrontmatter, extractMdxEsm, } from './extract-script-source.js';
64
+ import { parseImports } from './parse-imports.js';
65
+ export { resolveBarrelExport } from './resolve-barrel.js';
66
+ const EXTENSION_MAP = {
67
+ '.vue': 'vue',
68
+ '.svelte': 'svelte',
69
+ '.astro': 'astro',
70
+ '.mdx': 'mdx',
71
+ };
72
+ /**
73
+ * Determines the framework type from the file extension.
74
+ * Local to import-resolver to include MDX without affecting templateScanner.
75
+ */
76
+ function getImportFrameworkType(filePath) {
77
+ const ext = path.extname(filePath).toLowerCase();
78
+ return EXTENSION_MAP[ext] ?? null;
79
+ }
80
+ /**
81
+ * 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).
84
+ *
85
+ * @param filePath - The absolute or relative file path (used for framework detection)
86
+ * @param source - The full source text of the component file
87
+ * @returns The analysis result with all import bindings, or `null` if the framework
88
+ * is not supported or no relevant script block is found
89
+ */
90
+ export async function analyzeImports(filePath, source) {
91
+ const framework = getImportFrameworkType(filePath);
92
+ if (!framework) {
93
+ return null;
94
+ }
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;
117
+ }
118
+ }
119
+ if (!scriptSource) {
120
+ return { bindings: [] };
121
+ }
122
+ const bindings = await parseImports(scriptSource.content);
123
+ return { bindings };
124
+ }
125
+ /**
126
+ * Analyzes a Vue SFC's regular `<script>` block for Options API component registration.
127
+ * Parses all imports from the script block, then filters to only those whose
128
+ * local name appears in the `components: { ... }` property.
129
+ *
130
+ * @param source - The full Vue SFC source text
131
+ * @returns The analysis result with filtered bindings, or empty bindings if not applicable
132
+ */
133
+ async function analyzeVueOptionsApi(source) {
134
+ const scriptBlock = extractVueScript(source);
135
+ if (!scriptBlock) {
136
+ return { bindings: [] };
137
+ }
138
+ const allBindings = await parseImports(scriptBlock.content);
139
+ const componentNames = extractVueOptionsApiComponents(scriptBlock.content);
140
+ if (componentNames.length === 0) {
141
+ return { bindings: [] };
142
+ }
143
+ const componentNameSet = new Set(componentNames);
144
+ const bindings = allBindings.filter(b => componentNameSet.has(b.localName));
145
+ return { bindings };
146
+ }
147
+ /**
148
+ * Resolves a component name used in a template to its import source path.
149
+ * For Vue, handles both PascalCase (`<MyButton>`) and kebab-case (`<my-button>`)
150
+ * representations of the same component by normalizing to PascalCase for lookup.
151
+ *
152
+ * @param componentName - The component name as used in the template
153
+ * @param bindings - The import bindings extracted from the script block
154
+ * @returns The matching import binding, or `undefined` if no match is found
155
+ */
156
+ export function resolveComponentImport(componentName, bindings) {
157
+ // Direct match
158
+ const direct = bindings.find(b => b.localName === componentName);
159
+ if (direct) {
160
+ return direct;
161
+ }
162
+ // Vue kebab-case → PascalCase normalization
163
+ const pascalName = kebabToPascalCase(componentName);
164
+ if (pascalName !== componentName) {
165
+ return bindings.find(b => b.localName === pascalName);
166
+ }
167
+ return undefined;
168
+ }
169
+ /**
170
+ * 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
+ */
176
+ function kebabToPascalCase(str) {
177
+ if (!str.includes('-')) {
178
+ return str;
179
+ }
180
+ return str
181
+ .split('-')
182
+ .map(part => (part.length > 0 ? part[0].toUpperCase() + part.slice(1) : ''))
183
+ .join('');
184
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @module parse-imports
3
+ *
4
+ * Extracts import bindings from ESM source text using es-module-lexer.
5
+ * Since es-module-lexer provides specifier positions but not local binding names,
6
+ * regex parsing on the statement slices supplements the extraction.
7
+ */
8
+ import type { ImportBinding } from './types.js';
9
+ /**
10
+ * Analyzes source text and extracts import bindings using es-module-lexer.
11
+ *
12
+ * Processes static imports and dynamic imports with string literal specifiers.
13
+ * `import.meta` references and dynamic imports with non-literal specifiers
14
+ * (template literals, variables) are excluded.
15
+ *
16
+ * @param source - The source text to analyze (e.g., content of a `<script setup>` block)
17
+ * @returns An array of all import bindings found in the source
18
+ */
19
+ export declare function parseImports(source: string): Promise<readonly ImportBinding[]>;