@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.
- package/CHANGELOG.md +25 -0
- package/README.md +207 -62
- package/lib/cli.js +2 -3
- package/lib/dependency-mapper.d.ts +18 -35
- package/lib/dependency-mapper.js +40 -50
- package/lib/import-resolver/extract-script-source.d.ts +82 -0
- package/lib/import-resolver/extract-script-source.js +227 -0
- package/lib/import-resolver/index.d.ts +85 -0
- package/lib/import-resolver/index.js +184 -0
- package/lib/import-resolver/parse-imports.d.ts +19 -0
- package/lib/import-resolver/parse-imports.js +194 -0
- package/lib/import-resolver/resolve-barrel.d.ts +19 -0
- package/lib/import-resolver/resolve-barrel.js +113 -0
- package/lib/import-resolver/types.d.ts +34 -0
- package/lib/import-resolver/types.js +1 -0
- package/lib/index.d.ts +26 -1
- package/lib/index.js +24 -1
- package/lib/jsx/create-identify.d.ts +3 -4
- package/lib/jsx/create-identify.js +7 -22
- package/lib/jsx/get-children.d.ts +12 -6
- package/lib/jsx/get-children.js +64 -8
- package/lib/jsx/index.d.ts +4 -0
- package/lib/jsx/index.js +13 -5
- package/lib/pretender-director.d.ts +13 -4
- package/lib/pretender-director.js +15 -6
- package/lib/scan.d.ts +20 -0
- package/lib/scan.js +24 -0
- package/lib/template/derive-name.d.ts +12 -0
- package/lib/template/derive-name.js +27 -0
- package/lib/template/detect-slots.d.ts +14 -0
- package/lib/template/detect-slots.js +23 -0
- package/lib/template/extract-attrs.d.ts +13 -0
- package/lib/template/extract-attrs.js +26 -0
- package/lib/template/extract-root.d.ts +11 -0
- package/lib/template/extract-root.js +17 -0
- package/lib/template/index.d.ts +13 -0
- package/lib/template/index.js +51 -0
- package/lib/template/parse-component.d.ts +22 -0
- package/lib/template/parse-component.js +74 -0
- package/lib/template/types.d.ts +6 -0
- package/lib/template/types.js +1 -0
- package/lib/types.d.ts +7 -0
- 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[]>;
|