@contentful/experience-design-system-extraction 2.33.1-dev-build-3d84cf3.0 → 2.33.1-dev-build-c4ed223.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/dist/package.json +1 -1
- package/dist/src/extract/astro.js +40 -102
- package/dist/src/extract/file-extraction-workers.d.ts +25 -0
- package/dist/src/extract/file-extraction-workers.js +48 -0
- package/dist/src/extract/non-authorable-filter.d.ts +2 -1
- package/dist/src/extract/parse-imported-names.d.ts +1 -0
- package/dist/src/extract/parse-imported-names.js +27 -0
- package/dist/src/extract/pipeline.d.ts +2 -1
- package/dist/src/extract/react.js +156 -392
- package/dist/src/extract/resolve-local-module.d.ts +3 -0
- package/dist/src/extract/resolve-local-module.js +26 -0
- package/dist/src/extract/resolve-type-property.d.ts +16 -0
- package/dist/src/extract/resolve-type-property.js +11 -0
- package/dist/src/extract/slot-detection.js +11 -14
- package/dist/src/extract/source-inspection.js +1 -27
- package/dist/src/extract/source-line-metadata.d.ts +3 -0
- package/dist/src/extract/source-line-metadata.js +13 -0
- package/dist/src/extract/stencil.js +10 -26
- package/dist/src/extract/svelte.js +11 -58
- package/dist/src/extract/tsx-shared.d.ts +9 -1
- package/dist/src/extract/tsx-shared.js +48 -1
- package/dist/src/extract/validate.d.ts +4 -1
- package/dist/src/extract/validate.js +10 -5
- package/dist/src/extract/vue-tsx.js +20 -43
- package/dist/src/extract/vue.js +17 -64
- package/dist/src/extract/web-components.js +26 -39
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
export function resolveLocalModule(importingFilePath, specifier, options = {}) {
|
|
4
|
+
const basePath = resolve(dirname(importingFilePath), specifier);
|
|
5
|
+
const candidates = [
|
|
6
|
+
basePath,
|
|
7
|
+
`${basePath}.js`,
|
|
8
|
+
`${basePath}.ts`,
|
|
9
|
+
`${basePath}.mjs`,
|
|
10
|
+
`${basePath}.cjs`,
|
|
11
|
+
join(basePath, 'index.js'),
|
|
12
|
+
join(basePath, 'index.ts'),
|
|
13
|
+
join(basePath, 'index.mjs'),
|
|
14
|
+
join(basePath, 'index.cjs'),
|
|
15
|
+
];
|
|
16
|
+
for (const candidate of candidates) {
|
|
17
|
+
if (existsSync(candidate) && statSync(candidate).isFile())
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
if (options.allowJavaScriptExtensionFallback && basePath.endsWith('.js')) {
|
|
21
|
+
const tsPath = `${basePath.slice(0, -'.js'.length)}.ts`;
|
|
22
|
+
if (existsSync(tsPath) && statSync(tsPath).isFile())
|
|
23
|
+
return tsPath;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Node, Type } from 'ts-morph';
|
|
2
|
+
type TypeProperty = {
|
|
3
|
+
getName(): string;
|
|
4
|
+
getValueDeclaration(): Node | undefined;
|
|
5
|
+
getDeclarations(): Node[];
|
|
6
|
+
getTypeAtLocation(node: Node): Type;
|
|
7
|
+
isOptional(): boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ResolvedTypeProperty = {
|
|
10
|
+
name: string;
|
|
11
|
+
declaration: Node;
|
|
12
|
+
typeText: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function resolveTypeProperty(property: TypeProperty): ResolvedTypeProperty | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function resolveTypeProperty(property) {
|
|
2
|
+
const declaration = property.getValueDeclaration() ?? property.getDeclarations()[0];
|
|
3
|
+
if (!declaration)
|
|
4
|
+
return null;
|
|
5
|
+
return {
|
|
6
|
+
name: property.getName(),
|
|
7
|
+
declaration,
|
|
8
|
+
typeText: property.getTypeAtLocation(declaration).getText(declaration),
|
|
9
|
+
required: !property.isOptional(),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -28,18 +28,21 @@ export const CONTENT_NAME_EXCEPTIONS = new Set([
|
|
|
28
28
|
'excerpt',
|
|
29
29
|
]);
|
|
30
30
|
const REACT_NODE_EXACT_PATTERNS = ['ReactNode', 'React.ReactNode', 'ReactElement', 'React.ReactElement', 'JSX.Element'];
|
|
31
|
+
function normalizeReactNodeType(typeText) {
|
|
32
|
+
return typeText
|
|
33
|
+
.replace(/\s+/g, ' ')
|
|
34
|
+
.trim()
|
|
35
|
+
.split('|')
|
|
36
|
+
.map((part) => part.trim())
|
|
37
|
+
.filter((part) => part !== 'null' && part !== 'undefined')
|
|
38
|
+
.join(' | ');
|
|
39
|
+
}
|
|
31
40
|
/**
|
|
32
41
|
* Checks if a type string represents a ReactNode/ReactElement/JSX.Element type,
|
|
33
42
|
* including unions with null/undefined.
|
|
34
43
|
*/
|
|
35
44
|
export function isReactNodeType(typeText) {
|
|
36
|
-
const
|
|
37
|
-
// Strip optional markers and union with null/undefined
|
|
38
|
-
const stripped = normalized
|
|
39
|
-
.split('|')
|
|
40
|
-
.map((part) => part.trim())
|
|
41
|
-
.filter((part) => part !== 'null' && part !== 'undefined')
|
|
42
|
-
.join(' | ');
|
|
45
|
+
const stripped = normalizeReactNodeType(typeText);
|
|
43
46
|
// Check exact match (after stripping null/undefined union members)
|
|
44
47
|
if (REACT_NODE_EXACT_PATTERNS.includes(stripped)) {
|
|
45
48
|
return true;
|
|
@@ -55,13 +58,7 @@ export function isReactNodeType(typeText) {
|
|
|
55
58
|
* Examples: ReactNode[], React.ReactNode[], Array<ReactNode>
|
|
56
59
|
*/
|
|
57
60
|
export function isArrayReactNodeType(typeText) {
|
|
58
|
-
const
|
|
59
|
-
// Strip optional markers and union with null/undefined
|
|
60
|
-
const stripped = normalized
|
|
61
|
-
.split('|')
|
|
62
|
-
.map((part) => part.trim())
|
|
63
|
-
.filter((part) => part !== 'null' && part !== 'undefined')
|
|
64
|
-
.join(' | ');
|
|
61
|
+
const stripped = normalizeReactNodeType(typeText);
|
|
65
62
|
// Pattern: ReactNode[] or React.ReactNode[] etc.
|
|
66
63
|
for (const pattern of REACT_NODE_EXACT_PATTERNS) {
|
|
67
64
|
if (stripped === `${pattern}[]`) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseImportedNames } from './parse-imported-names.js';
|
|
2
3
|
export const HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON = 'data-fetch-wrapper';
|
|
3
4
|
export const POSSIBLE_DATA_FETCH_WRAPPER_REASON = 'possible-data-fetch-wrapper';
|
|
4
5
|
export const ZERO_SURFACE_RENDERED_UI_REASON = 'zero-surface:rendered-ui';
|
|
@@ -13,33 +14,6 @@ const IMPORT_PATTERN = /import\s+(?:type\s+)?(.+?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
|
13
14
|
function escapeRegExp(value) {
|
|
14
15
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
16
|
}
|
|
16
|
-
function parseImportedNames(importClause) {
|
|
17
|
-
const trimmed = importClause.trim();
|
|
18
|
-
if (!trimmed)
|
|
19
|
-
return [];
|
|
20
|
-
const names = [];
|
|
21
|
-
const defaultAndNamed = trimmed.split(',').map((part) => part.trim());
|
|
22
|
-
for (const part of defaultAndNamed) {
|
|
23
|
-
if (!part)
|
|
24
|
-
continue;
|
|
25
|
-
if (part.startsWith('{') && part.endsWith('}')) {
|
|
26
|
-
for (const named of part
|
|
27
|
-
.slice(1, -1)
|
|
28
|
-
.split(',')
|
|
29
|
-
.map((item) => item.trim())
|
|
30
|
-
.filter(Boolean)) {
|
|
31
|
-
const [local] = named.split(/\s+as\s+/i);
|
|
32
|
-
if (local)
|
|
33
|
-
names.push(local.trim());
|
|
34
|
-
}
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const [local] = part.split(/\s+as\s+/i);
|
|
38
|
-
if (local)
|
|
39
|
-
names.push(local.trim());
|
|
40
|
-
}
|
|
41
|
-
return names;
|
|
42
|
-
}
|
|
43
17
|
function collectSiblingRendererImports(sourceText) {
|
|
44
18
|
const names = new Set();
|
|
45
19
|
for (const match of sourceText.matchAll(IMPORT_PATTERN)) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getSourceLineMetadata(declaration) {
|
|
2
|
+
if (!declaration)
|
|
3
|
+
return {};
|
|
4
|
+
const lineAwareDeclaration = declaration;
|
|
5
|
+
if (typeof lineAwareDeclaration.getStartLineNumber !== 'function' ||
|
|
6
|
+
typeof lineAwareDeclaration.getEndLineNumber !== 'function') {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
sourceStartLine: lineAwareDeclaration.getStartLineNumber(),
|
|
11
|
+
sourceEndLine: lineAwareDeclaration.getEndLineNumber(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import { Project, Node, SyntaxKind } from 'ts-morph';
|
|
2
|
-
import { getJsxTagNameNode, isIntrinsicJsxElement } from './tsx-shared.js';
|
|
2
|
+
import { getJsxTagNameNode, isIntrinsicJsxElement, kebabToPascal } from './tsx-shared.js';
|
|
3
|
+
import { createSortedExtractionResult, extractProjectSourceFiles } from './file-extraction-workers.js';
|
|
3
4
|
function isStencilFile(sourceFile) {
|
|
4
5
|
return sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === '@stencil/core');
|
|
5
6
|
}
|
|
6
|
-
function kebabToPascal(input) {
|
|
7
|
-
return input
|
|
8
|
-
.split('-')
|
|
9
|
-
.filter(Boolean)
|
|
10
|
-
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
11
|
-
.join('');
|
|
12
|
-
}
|
|
13
7
|
function getComponentTag(classDecl) {
|
|
14
8
|
for (const decorator of classDecl.getDecorators()) {
|
|
15
9
|
if (decorator.getName() !== 'Component')
|
|
@@ -307,22 +301,12 @@ export async function extractStencilComponents(filePaths) {
|
|
|
307
301
|
for (const filePath of tsxFiles) {
|
|
308
302
|
project.addSourceFileAtPath(filePath);
|
|
309
303
|
}
|
|
310
|
-
const warnings =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
detectFunctionalComponents(sourceFile, warnings);
|
|
319
|
-
}
|
|
320
|
-
catch (e) {
|
|
321
|
-
warnings.push(`Failed to extract from ${sourceFile.getFilePath()}: ${e instanceof Error ? e.message : String(e)}`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return {
|
|
325
|
-
components: components.sort((a, b) => a.name.localeCompare(b.name)),
|
|
326
|
-
warnings,
|
|
327
|
-
};
|
|
304
|
+
const { items: components, warnings } = extractProjectSourceFiles(project, (sourceFile, sourceWarnings) => {
|
|
305
|
+
if (!isStencilFile(sourceFile))
|
|
306
|
+
return [];
|
|
307
|
+
const extracted = extractFromSourceFile(sourceFile, sourceWarnings);
|
|
308
|
+
detectFunctionalComponents(sourceFile, sourceWarnings);
|
|
309
|
+
return extracted;
|
|
310
|
+
});
|
|
311
|
+
return createSortedExtractionResult(components, warnings);
|
|
328
312
|
}
|
|
@@ -1,47 +1,24 @@
|
|
|
1
1
|
import { basename, dirname, resolve, join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
3
|
import { createRequire } from 'node:module';
|
|
5
4
|
import os from 'node:os';
|
|
6
5
|
import { parse as parseSvelte } from 'svelte/compiler';
|
|
7
6
|
import { Project, Node, ScriptTarget, ModuleKind, ts } from 'ts-morph';
|
|
8
7
|
import { computeExtractionScore, deriveNeedsReview } from './scoring.js';
|
|
9
8
|
import { extractAllowedComponentsFromTypeText } from './slot-allowed-components.js';
|
|
9
|
+
import { runFileExtractionWorkers } from './file-extraction-workers.js';
|
|
10
|
+
import { resolveLocalModule } from './resolve-local-module.js';
|
|
10
11
|
const SVELTE_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
|
|
11
12
|
export async function extractSvelteComponents(filePaths, onProgress, opts) {
|
|
12
13
|
const svelteFiles = filePaths.filter((f) => f.endsWith('.svelte'));
|
|
13
|
-
if (svelteFiles.length === 0)
|
|
14
|
-
return { components: [], warnings: [] };
|
|
15
|
-
const warnings = [];
|
|
16
|
-
const components = [];
|
|
17
14
|
const retryContexts = new Map();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
break;
|
|
26
|
-
try {
|
|
27
|
-
const source = await readFile(filePath, 'utf-8');
|
|
28
|
-
const { component, warnings: fileWarnings, retryContext } = await extractFromSvelteFile(filePath, source);
|
|
29
|
-
warnings.push(...fileWarnings);
|
|
30
|
-
if (component) {
|
|
31
|
-
components.push(component);
|
|
32
|
-
componentsFound++;
|
|
33
|
-
}
|
|
34
|
-
if (retryContext)
|
|
35
|
-
retryContexts.set(filePath, retryContext);
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
warnings.push(`${getSvelteComponentName(filePath)}: failed to extract from ${filePath} — ${e instanceof Error ? e.message : String(e)}`);
|
|
39
|
-
}
|
|
40
|
-
filesProcessed++;
|
|
41
|
-
onProgress?.({ filesProcessed, componentsFound });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
await Promise.all(Array.from({ length: Math.min(SVELTE_EXTRACT_CONCURRENCY, svelteFiles.length) }, worker));
|
|
15
|
+
const { items: components, warnings } = await runFileExtractionWorkers(svelteFiles, SVELTE_EXTRACT_CONCURRENCY, async (filePath, source) => {
|
|
16
|
+
const { component, warnings: fileWarnings, retryContext } = await extractFromSvelteFile(filePath, source);
|
|
17
|
+
return { item: component, warnings: fileWarnings, metadata: retryContext };
|
|
18
|
+
}, (filePath, error) => `${getSvelteComponentName(filePath)}: failed to extract from ${filePath} — ${error instanceof Error ? error.message : String(error)}`, onProgress, (filePath, outcome) => {
|
|
19
|
+
if (outcome.metadata)
|
|
20
|
+
retryContexts.set(filePath, outcome.metadata);
|
|
21
|
+
});
|
|
45
22
|
const finalWarnings = await maybeRunResolveUnreachableRetry(components, warnings, retryContexts, opts);
|
|
46
23
|
resolveAllowedComponents(components);
|
|
47
24
|
return {
|
|
@@ -1155,37 +1132,13 @@ async function resolveImportedTypeMembers(typeName, instance, filePath) {
|
|
|
1155
1132
|
}
|
|
1156
1133
|
if (!importedExport)
|
|
1157
1134
|
continue;
|
|
1158
|
-
const resolvedFile =
|
|
1135
|
+
const resolvedFile = resolveLocalModule(filePath, specifierValue, { allowJavaScriptExtensionFallback: true });
|
|
1159
1136
|
if (!resolvedFile)
|
|
1160
1137
|
continue;
|
|
1161
1138
|
return readMembersFromExternalFile(resolvedFile, importedExport);
|
|
1162
1139
|
}
|
|
1163
1140
|
return null;
|
|
1164
1141
|
}
|
|
1165
|
-
function resolveLocalScriptModule(importingFilePath, specifier) {
|
|
1166
|
-
const basePath = resolve(dirname(importingFilePath), specifier);
|
|
1167
|
-
const candidates = [
|
|
1168
|
-
basePath,
|
|
1169
|
-
`${basePath}.js`,
|
|
1170
|
-
`${basePath}.ts`,
|
|
1171
|
-
`${basePath}.mjs`,
|
|
1172
|
-
`${basePath}.cjs`,
|
|
1173
|
-
join(basePath, 'index.js'),
|
|
1174
|
-
join(basePath, 'index.ts'),
|
|
1175
|
-
join(basePath, 'index.mjs'),
|
|
1176
|
-
join(basePath, 'index.cjs'),
|
|
1177
|
-
];
|
|
1178
|
-
for (const c of candidates) {
|
|
1179
|
-
if (existsSync(c) && statSync(c).isFile())
|
|
1180
|
-
return c;
|
|
1181
|
-
}
|
|
1182
|
-
if (basePath.endsWith('.js')) {
|
|
1183
|
-
const tsPath = `${basePath.slice(0, -'.js'.length)}.ts`;
|
|
1184
|
-
if (existsSync(tsPath) && statSync(tsPath).isFile())
|
|
1185
|
-
return tsPath;
|
|
1186
|
-
}
|
|
1187
|
-
return null;
|
|
1188
|
-
}
|
|
1189
1142
|
function readMembersFromExternalFile(filePath, exportName) {
|
|
1190
1143
|
const project = new Project({
|
|
1191
1144
|
compilerOptions: {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import { Node, type Type } from 'ts-morph';
|
|
1
|
+
import { Node, Project, type Type } from 'ts-morph';
|
|
2
|
+
export declare function getTsxExtractionContext(filePaths: string[], componentFilePattern: RegExp): {
|
|
3
|
+
componentFiles: string[];
|
|
4
|
+
project: Project;
|
|
5
|
+
} | undefined;
|
|
6
|
+
export declare function resolveDefaultExportName(declarations: Node[], exported: {
|
|
7
|
+
has(name: string): boolean;
|
|
8
|
+
}, allowVariableDeclaration?: boolean): string | undefined;
|
|
9
|
+
export declare function kebabToPascal(input: string): string;
|
|
2
10
|
export declare function extractAllowedValues(type: Type): string[] | undefined;
|
|
3
11
|
/**
|
|
4
12
|
* Custom elements include a hyphen. Lowercase, unqualified tags are therefore
|
|
@@ -1,11 +1,58 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { Node, SyntaxKind } from 'ts-morph';
|
|
3
|
+
import { Node, Project, SyntaxKind } from 'ts-morph';
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
const packageRootByFilePathCache = new Map();
|
|
6
6
|
const workspacePackageManifestCache = new Map();
|
|
7
7
|
const nearestTsConfigPathCache = new Map();
|
|
8
8
|
const tsConfigPathsCache = new Map();
|
|
9
|
+
function createTsxProject(filePaths) {
|
|
10
|
+
const project = new Project({
|
|
11
|
+
compilerOptions: {
|
|
12
|
+
jsx: 1,
|
|
13
|
+
target: 99,
|
|
14
|
+
module: 99,
|
|
15
|
+
moduleResolution: 100,
|
|
16
|
+
skipLibCheck: true,
|
|
17
|
+
allowJs: true,
|
|
18
|
+
},
|
|
19
|
+
skipAddingFilesFromTsConfig: true,
|
|
20
|
+
});
|
|
21
|
+
for (const filePath of filePaths) {
|
|
22
|
+
project.addSourceFileAtPath(filePath);
|
|
23
|
+
}
|
|
24
|
+
return project;
|
|
25
|
+
}
|
|
26
|
+
function getTsxProjectFiles(filePaths) {
|
|
27
|
+
return filePaths.filter((filePath) => /\.[jt]sx?$/.test(filePath) && !filePath.endsWith('.d.ts'));
|
|
28
|
+
}
|
|
29
|
+
export function getTsxExtractionContext(filePaths, componentFilePattern) {
|
|
30
|
+
const componentFiles = filePaths.filter((filePath) => componentFilePattern.test(filePath));
|
|
31
|
+
if (componentFiles.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
componentFiles,
|
|
35
|
+
project: createTsxProject(getTsxProjectFiles(filePaths)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function resolveDefaultExportName(declarations, exported, allowVariableDeclaration = false) {
|
|
39
|
+
const declaration = declarations[0];
|
|
40
|
+
const name = Node.isFunctionDeclaration(declaration)
|
|
41
|
+
? declaration.getName()
|
|
42
|
+
: allowVariableDeclaration && Node.isVariableDeclaration(declaration)
|
|
43
|
+
? declaration.getName()
|
|
44
|
+
: undefined;
|
|
45
|
+
if (!name || !/^[A-Z]/.test(name) || exported.has(name))
|
|
46
|
+
return undefined;
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
export function kebabToPascal(input) {
|
|
50
|
+
return input
|
|
51
|
+
.split('-')
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
54
|
+
.join('');
|
|
55
|
+
}
|
|
9
56
|
export function extractAllowedValues(type) {
|
|
10
57
|
if (!type.isUnion())
|
|
11
58
|
return undefined;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { RawComponentDefinition, ExtractionValidationIssue } from '../types.js';
|
|
2
|
-
export type { ExtractionValidationIssue } from '../types.js';
|
|
3
2
|
export declare function validateExtractedComponents(components: RawComponentDefinition[]): RawComponentDefinition[];
|
|
4
3
|
export declare function shouldExcludeDueToValidation(component: RawComponentDefinition): boolean;
|
|
5
4
|
/**
|
|
@@ -14,3 +13,7 @@ export declare function formatExclusionWarning(rejected: Array<{
|
|
|
14
13
|
name: string;
|
|
15
14
|
validationIssues?: ExtractionValidationIssue[];
|
|
16
15
|
}>): string;
|
|
16
|
+
export declare function formatExcludedComponentLines(rejected: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
validationIssues?: ExtractionValidationIssue[];
|
|
19
|
+
}>): string[];
|
|
@@ -82,13 +82,18 @@ export function shouldExcludeDueToValidation(component) {
|
|
|
82
82
|
export function formatExclusionWarning(rejected) {
|
|
83
83
|
if (rejected.length === 0)
|
|
84
84
|
return '';
|
|
85
|
-
const lines = [
|
|
86
|
-
|
|
85
|
+
const lines = [
|
|
86
|
+
`Warning: ${rejected.length} component(s) excluded due to validation errors:`,
|
|
87
|
+
...formatExcludedComponentLines(rejected),
|
|
88
|
+
];
|
|
89
|
+
return lines.join('\n') + '\n';
|
|
90
|
+
}
|
|
91
|
+
export function formatExcludedComponentLines(rejected) {
|
|
92
|
+
return rejected.map((comp) => {
|
|
87
93
|
const codes = (comp.validationIssues ?? [])
|
|
88
94
|
.filter((i) => i.severity === 'error')
|
|
89
95
|
.map((i) => i.code)
|
|
90
96
|
.join(', ');
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
return lines.join('\n') + '\n';
|
|
97
|
+
return ` ✗ ${comp.name} ${codes}`;
|
|
98
|
+
});
|
|
94
99
|
}
|
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
import { Node,
|
|
2
|
-
import { extractAllowedValues, getTypeReferenceName, getTypeTargetDeclarations, getValueTargetDeclarations, } from './tsx-shared.js';
|
|
1
|
+
import { Node, SyntaxKind } from 'ts-morph';
|
|
2
|
+
import { extractAllowedValues, getTypeReferenceName, getTypeTargetDeclarations, getTsxExtractionContext, getValueTargetDeclarations, resolveDefaultExportName, } from './tsx-shared.js';
|
|
3
3
|
export async function extractVueTsxComponents(filePaths) {
|
|
4
|
-
const
|
|
5
|
-
if (
|
|
4
|
+
const extractionContext = getTsxExtractionContext(filePaths, /\.tsx$/);
|
|
5
|
+
if (!extractionContext) {
|
|
6
6
|
return { components: [], warnings: [] };
|
|
7
7
|
}
|
|
8
|
-
const
|
|
9
|
-
const project = new Project({
|
|
10
|
-
compilerOptions: {
|
|
11
|
-
jsx: 1,
|
|
12
|
-
target: 99,
|
|
13
|
-
module: 99,
|
|
14
|
-
moduleResolution: 100,
|
|
15
|
-
skipLibCheck: true,
|
|
16
|
-
allowJs: true,
|
|
17
|
-
},
|
|
18
|
-
skipAddingFilesFromTsConfig: true,
|
|
19
|
-
});
|
|
20
|
-
for (const filePath of projectFiles) {
|
|
21
|
-
project.addSourceFileAtPath(filePath);
|
|
22
|
-
}
|
|
8
|
+
const { componentFiles, project } = extractionContext;
|
|
23
9
|
const warnings = [];
|
|
24
10
|
const components = [];
|
|
25
11
|
for (const filePath of componentFiles) {
|
|
@@ -44,13 +30,10 @@ function extractFromSourceFile(sourceFile) {
|
|
|
44
30
|
for (const [exportKey, declarations] of exported) {
|
|
45
31
|
let name = exportKey;
|
|
46
32
|
if (exportKey === 'default') {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (!declName || !/^[A-Z]/.test(declName))
|
|
50
|
-
continue;
|
|
51
|
-
if (exported.has(declName))
|
|
33
|
+
const defaultExportName = resolveDefaultExportName(declarations, exported);
|
|
34
|
+
if (!defaultExportName)
|
|
52
35
|
continue;
|
|
53
|
-
name =
|
|
36
|
+
name = defaultExportName;
|
|
54
37
|
}
|
|
55
38
|
if (!/^[A-Z]/.test(name))
|
|
56
39
|
continue;
|
|
@@ -415,20 +398,22 @@ function extractStringLiteralArrayValues(node) {
|
|
|
415
398
|
}
|
|
416
399
|
return keys;
|
|
417
400
|
}
|
|
401
|
+
function extractSlotMembers(members, allowMethods) {
|
|
402
|
+
return members
|
|
403
|
+
.flatMap((member) => {
|
|
404
|
+
if (!Node.isPropertySignature(member) && (!allowMethods || !Node.isMethodSignature(member)))
|
|
405
|
+
return [];
|
|
406
|
+
const slotName = member.getName();
|
|
407
|
+
return [{ name: slotName, isDefault: slotName === 'default' }];
|
|
408
|
+
})
|
|
409
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
410
|
+
}
|
|
418
411
|
function extractVueSlotsFromTypeNode(typeNode, seen) {
|
|
419
412
|
if (!typeNode || seen.has(typeNode))
|
|
420
413
|
return [];
|
|
421
414
|
seen.add(typeNode);
|
|
422
415
|
if (Node.isTypeLiteral(typeNode)) {
|
|
423
|
-
return typeNode
|
|
424
|
-
.getMembers()
|
|
425
|
-
.flatMap((member) => {
|
|
426
|
-
if (!Node.isPropertySignature(member))
|
|
427
|
-
return [];
|
|
428
|
-
const slotName = member.getName();
|
|
429
|
-
return [{ name: slotName, isDefault: slotName === 'default' }];
|
|
430
|
-
})
|
|
431
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
416
|
+
return extractSlotMembers(typeNode.getMembers(), false);
|
|
432
417
|
}
|
|
433
418
|
if (Node.isTypeReference(typeNode)) {
|
|
434
419
|
for (const declaration of getTypeTargetDeclarations(typeNode.getTypeName(), false)) {
|
|
@@ -436,15 +421,7 @@ function extractVueSlotsFromTypeNode(typeNode, seen) {
|
|
|
436
421
|
return extractVueSlotsFromTypeNode(declaration.getTypeNode(), seen);
|
|
437
422
|
}
|
|
438
423
|
if (Node.isInterfaceDeclaration(declaration)) {
|
|
439
|
-
return declaration
|
|
440
|
-
.getMembers()
|
|
441
|
-
.flatMap((member) => {
|
|
442
|
-
if (!Node.isPropertySignature(member) && !Node.isMethodSignature(member))
|
|
443
|
-
return [];
|
|
444
|
-
const slotName = member.getName();
|
|
445
|
-
return [{ name: slotName, isDefault: slotName === 'default' }];
|
|
446
|
-
})
|
|
447
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
424
|
+
return extractSlotMembers(declaration.getMembers(), true);
|
|
448
425
|
}
|
|
449
426
|
}
|
|
450
427
|
}
|
package/dist/src/extract/vue.js
CHANGED
|
@@ -4,6 +4,9 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import { parse as parseSFC } from '@vue/compiler-sfc';
|
|
6
6
|
import { Project, Node } from 'ts-morph';
|
|
7
|
+
import { createSortedExtractionResult, runFileExtractionWorkers } from './file-extraction-workers.js';
|
|
8
|
+
import { resolveLocalModule } from './resolve-local-module.js';
|
|
9
|
+
import { resolveTypeProperty } from './resolve-type-property.js';
|
|
7
10
|
// @vue/compiler-core NodeTypes enum values (stable since Vue 3.0)
|
|
8
11
|
const ELEMENT_TYPE = 1;
|
|
9
12
|
const ATTRIBUTE_TYPE = 6;
|
|
@@ -20,40 +23,11 @@ const VUE_TYPE_MAP = {
|
|
|
20
23
|
const VUE_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
|
|
21
24
|
export async function extractVueComponents(filePaths, onProgress) {
|
|
22
25
|
const vueFiles = filePaths.filter((f) => f.endsWith('.vue'));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let filesProcessed = 0;
|
|
29
|
-
let componentsFound = 0;
|
|
30
|
-
const queue = [...vueFiles];
|
|
31
|
-
async function worker() {
|
|
32
|
-
while (queue.length > 0) {
|
|
33
|
-
const filePath = queue.shift();
|
|
34
|
-
if (!filePath)
|
|
35
|
-
break;
|
|
36
|
-
try {
|
|
37
|
-
const source = await readFile(filePath, 'utf-8');
|
|
38
|
-
const { component, warnings: fileWarnings } = await extractFromVueSFC(filePath, source);
|
|
39
|
-
warnings.push(...fileWarnings);
|
|
40
|
-
if (component) {
|
|
41
|
-
components.push(component);
|
|
42
|
-
componentsFound++;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (e) {
|
|
46
|
-
warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
|
-
}
|
|
48
|
-
filesProcessed++;
|
|
49
|
-
onProgress?.({ filesProcessed, componentsFound });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
await Promise.all(Array.from({ length: Math.min(VUE_EXTRACT_CONCURRENCY, vueFiles.length) }, worker));
|
|
53
|
-
return {
|
|
54
|
-
components: components.sort((a, b) => a.name.localeCompare(b.name)),
|
|
55
|
-
warnings,
|
|
56
|
-
};
|
|
26
|
+
const { items: components, warnings } = await runFileExtractionWorkers(vueFiles, VUE_EXTRACT_CONCURRENCY, async (filePath, source) => {
|
|
27
|
+
const { component, warnings: fileWarnings } = await extractFromVueSFC(filePath, source);
|
|
28
|
+
return { item: component, warnings: fileWarnings };
|
|
29
|
+
}, (filePath, error) => `Failed to extract from ${filePath}: ${error instanceof Error ? error.message : String(error)}`, onProgress);
|
|
30
|
+
return createSortedExtractionResult(components, warnings);
|
|
57
31
|
}
|
|
58
32
|
async function extractFromVueSFC(filePath, source) {
|
|
59
33
|
const { descriptor, errors } = parseSFC(source);
|
|
@@ -179,20 +153,19 @@ function extractGenericProps(typeText) {
|
|
|
179
153
|
const type = typeAlias.getType();
|
|
180
154
|
const props = [];
|
|
181
155
|
for (const property of type.getProperties()) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
if (!decl)
|
|
156
|
+
const resolved = resolveTypeProperty(property);
|
|
157
|
+
if (!resolved)
|
|
185
158
|
continue;
|
|
186
|
-
const
|
|
159
|
+
const { name, typeText: resolvedTypeText, required } = resolved;
|
|
187
160
|
// Strip `| undefined` from optional types for clean output
|
|
188
|
-
let
|
|
189
|
-
if (
|
|
190
|
-
|
|
161
|
+
let typeText = resolvedTypeText;
|
|
162
|
+
if (!required) {
|
|
163
|
+
typeText = typeText.replace(/\s*\|\s*undefined$/, '').replace(/^undefined\s*\|\s*/, '');
|
|
191
164
|
}
|
|
192
165
|
props.push({
|
|
193
166
|
name,
|
|
194
|
-
type:
|
|
195
|
-
required
|
|
167
|
+
type: typeText,
|
|
168
|
+
required,
|
|
196
169
|
});
|
|
197
170
|
}
|
|
198
171
|
return props.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -203,7 +176,7 @@ function collectSetupImportedObjectRefs(sf, filePath) {
|
|
|
203
176
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
204
177
|
if (!specifier.startsWith('.'))
|
|
205
178
|
continue;
|
|
206
|
-
const resolvedImportPath =
|
|
179
|
+
const resolvedImportPath = resolveLocalModule(filePath, specifier);
|
|
207
180
|
if (!resolvedImportPath)
|
|
208
181
|
continue;
|
|
209
182
|
for (const namedImport of importDecl.getNamedImports()) {
|
|
@@ -215,26 +188,6 @@ function collectSetupImportedObjectRefs(sf, filePath) {
|
|
|
215
188
|
}
|
|
216
189
|
return refs;
|
|
217
190
|
}
|
|
218
|
-
function resolveLocalScriptModule(importingFilePath, specifier) {
|
|
219
|
-
const basePath = resolve(dirname(importingFilePath), specifier);
|
|
220
|
-
const candidates = [
|
|
221
|
-
basePath,
|
|
222
|
-
`${basePath}.js`,
|
|
223
|
-
`${basePath}.ts`,
|
|
224
|
-
`${basePath}.mjs`,
|
|
225
|
-
`${basePath}.cjs`,
|
|
226
|
-
join(basePath, 'index.js'),
|
|
227
|
-
join(basePath, 'index.ts'),
|
|
228
|
-
join(basePath, 'index.mjs'),
|
|
229
|
-
join(basePath, 'index.cjs'),
|
|
230
|
-
];
|
|
231
|
-
for (const candidate of candidates) {
|
|
232
|
-
if (existsSync(candidate) && statSync(candidate).isFile()) {
|
|
233
|
-
return candidate;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
191
|
async function parseSetupObjectProps(obj, importedObjectRefs, visitedImports) {
|
|
239
192
|
const mergedProps = new Map();
|
|
240
193
|
for (const prop of parseObjectProps(obj)) {
|