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