@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 +8 -0
- package/README.md +6 -6
- package/lib/component-scanner.d.ts +67 -0
- package/lib/component-scanner.js +9 -0
- package/lib/import-resolver/extract-script-source.d.ts +10 -33
- package/lib/import-resolver/extract-script-source.js +9 -93
- package/lib/import-resolver/index.d.ts +7 -37
- package/lib/import-resolver/index.js +33 -65
- package/lib/index.d.ts +1 -0
- package/lib/pretender-director.d.ts +1 -1
- package/lib/scan.d.ts +1 -1
- package/lib/scan.js +1 -1
- package/lib/scanner-loader.d.ts +8 -0
- package/lib/scanner-loader.js +56 -0
- package/lib/template/index.d.ts +3 -3
- package/lib/template/index.js +25 -19
- package/package.json +14 -10
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
|
|
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>` (
|
|
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
|
-
*
|
|
5
|
-
* Each framework stores imports in different locations:
|
|
4
|
+
* Script source extractors for component files.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* |
|
|
12
|
-
*
|
|
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
|
-
*
|
|
5
|
-
* Each framework stores imports in different locations:
|
|
4
|
+
* Script source extractors for component files.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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>` (
|
|
14
|
-
* - Vue Options API `components` property (fallback
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
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
|
|
68
|
-
*
|
|
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>` (
|
|
14
|
-
* - Vue Options API `components` property (fallback
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
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 {
|
|
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
|
|
83
|
-
*
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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("
|
|
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
|
+
}
|
package/lib/template/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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)
|
package/lib/template/index.js
CHANGED
|
@@ -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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
30
|
-
|
|
27
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
28
|
+
const scanner = await getScanner(ext);
|
|
29
|
+
if (!scanner) {
|
|
31
30
|
continue;
|
|
32
31
|
}
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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:
|
|
49
|
+
element: scan.rootElement,
|
|
44
50
|
...(attrs.length > 0 ? { attrs } : {}),
|
|
45
|
-
slots: hasSlots ? true : null,
|
|
51
|
+
slots: scan.hasSlots ? true : null,
|
|
46
52
|
}
|
|
47
|
-
:
|
|
48
|
-
director.add(componentName, identity, 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.
|
|
3
|
+
"version": "5.0.0-rc.2",
|
|
4
4
|
"description": "It loads components and then creates the pretenders data from them.",
|
|
5
|
-
"repository":
|
|
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.
|
|
32
|
-
"@markuplint/ml-config": "5.0.0-rc.
|
|
33
|
-
"@markuplint/parser-utils": "5.0.0-rc.
|
|
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": "
|
|
41
|
+
"typescript": "6.0.2"
|
|
38
42
|
},
|
|
39
43
|
"optionalDependencies": {
|
|
40
|
-
"@markuplint/astro-parser": "5.0.0-rc.
|
|
41
|
-
"@markuplint/svelte-parser": "5.0.0-rc.
|
|
42
|
-
"@markuplint/vue-parser": "5.0.0-rc.
|
|
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": "
|
|
48
|
+
"gitHead": "e43763858d9234c417053becc73dbd088c1e7ea6"
|
|
45
49
|
}
|