@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.
@@ -0,0 +1,3 @@
1
+ export declare function resolveLocalModule(importingFilePath: string, specifier: string, options?: {
2
+ allowJavaScriptExtensionFallback?: boolean;
3
+ }): string | null;
@@ -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 normalized = typeText.replace(/\s+/g, ' ').trim();
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 normalized = typeText.replace(/\s+/g, ' ').trim();
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,3 @@
1
+ import type { Node } from 'ts-morph';
2
+ import type { RawPropDefinition } from '../types.js';
3
+ export declare function getSourceLineMetadata(declaration: Node | undefined): Pick<RawPropDefinition, 'sourceStartLine' | 'sourceEndLine'>;
@@ -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
- const components = [];
312
- for (const sourceFile of project.getSourceFiles()) {
313
- try {
314
- if (!isStencilFile(sourceFile))
315
- continue;
316
- const extracted = extractFromSourceFile(sourceFile, warnings);
317
- components.push(...extracted);
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 { readFile } from 'node:fs/promises';
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
- let filesProcessed = 0;
19
- let componentsFound = 0;
20
- const queue = [...svelteFiles];
21
- async function worker() {
22
- while (queue.length > 0) {
23
- const filePath = queue.shift();
24
- if (!filePath)
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 = resolveLocalScriptModule(filePath, specifierValue);
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 = [`Warning: ${rejected.length} component(s) excluded due to validation errors:`];
86
- for (const comp of rejected) {
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
- lines.push(` ✗ ${comp.name} ${codes}`);
92
- }
93
- return lines.join('\n') + '\n';
97
+ return ` ✗ ${comp.name} ${codes}`;
98
+ });
94
99
  }
@@ -1,25 +1,11 @@
1
- import { Node, Project, SyntaxKind } from 'ts-morph';
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 componentFiles = filePaths.filter((f) => f.endsWith('.tsx'));
5
- if (componentFiles.length === 0) {
4
+ const extractionContext = getTsxExtractionContext(filePaths, /\.tsx$/);
5
+ if (!extractionContext) {
6
6
  return { components: [], warnings: [] };
7
7
  }
8
- const projectFiles = filePaths.filter((f) => /\.[jt]sx?$/.test(f) && !f.endsWith('.d.ts'));
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 decl = declarations[0];
48
- const declName = Node.isFunctionDeclaration(decl) ? decl.getName() : undefined;
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 = declName;
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
  }
@@ -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
- if (vueFiles.length === 0) {
24
- return { components: [], warnings: [] };
25
- }
26
- const warnings = [];
27
- const components = [];
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 name = property.getName();
183
- const decl = property.getValueDeclaration() ?? property.getDeclarations()[0];
184
- if (!decl)
156
+ const resolved = resolveTypeProperty(property);
157
+ if (!resolved)
185
158
  continue;
186
- const propType = property.getTypeAtLocation(decl);
159
+ const { name, typeText: resolvedTypeText, required } = resolved;
187
160
  // Strip `| undefined` from optional types for clean output
188
- let resolvedTypeText = propType.getText(decl);
189
- if (property.isOptional()) {
190
- resolvedTypeText = resolvedTypeText.replace(/\s*\|\s*undefined$/, '').replace(/^undefined\s*\|\s*/, '');
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: resolvedTypeText,
195
- required: !property.isOptional(),
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 = resolveLocalScriptModule(filePath, specifier);
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)) {