@contentful/experience-design-system-cli 2.12.4-dev-build-ff57340.0 → 2.13.1-dev-build-fa478b3.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.
Files changed (55) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/analyze/command.js +2 -22
  3. package/dist/src/analyze/select/command.js +1 -1
  4. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +1 -24
  5. package/dist/src/analyze/select/tui/components/FieldEditor.js +10 -137
  6. package/dist/src/analyze/select-agent/command.js +1 -1
  7. package/dist/src/apply/command.d.ts +1 -34
  8. package/dist/src/apply/command.js +0 -81
  9. package/dist/src/import/tui/WizardApp.js +10 -59
  10. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
  11. package/dist/src/import/tui/steps/GenerateReviewStep.js +23 -213
  12. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
  13. package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
  14. package/dist/src/import/tui/steps/preview-diff.js +0 -48
  15. package/dist/src/session/db.d.ts +0 -9
  16. package/dist/src/session/db.js +0 -46
  17. package/dist/src/types.d.ts +2 -76
  18. package/dist/src/types.js +1 -4
  19. package/package.json +3 -6
  20. package/dist/src/analyze/cycle-detection.d.ts +0 -93
  21. package/dist/src/analyze/cycle-detection.js +0 -326
  22. package/dist/src/analyze/extract/astro.d.ts +0 -5
  23. package/dist/src/analyze/extract/astro.js +0 -289
  24. package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
  25. package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
  26. package/dist/src/analyze/extract/pipeline.d.ts +0 -6
  27. package/dist/src/analyze/extract/pipeline.js +0 -314
  28. package/dist/src/analyze/extract/react.d.ts +0 -2
  29. package/dist/src/analyze/extract/react.js +0 -2041
  30. package/dist/src/analyze/extract/scoring.d.ts +0 -12
  31. package/dist/src/analyze/extract/scoring.js +0 -108
  32. package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
  33. package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
  34. package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
  35. package/dist/src/analyze/extract/slot-detection.js +0 -101
  36. package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
  37. package/dist/src/analyze/extract/source-inspection.js +0 -189
  38. package/dist/src/analyze/extract/stencil.d.ts +0 -2
  39. package/dist/src/analyze/extract/stencil.js +0 -296
  40. package/dist/src/analyze/extract/svelte.d.ts +0 -5
  41. package/dist/src/analyze/extract/svelte.js +0 -1626
  42. package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
  43. package/dist/src/analyze/extract/tsx-shared.js +0 -263
  44. package/dist/src/analyze/extract/validate.d.ts +0 -16
  45. package/dist/src/analyze/extract/validate.js +0 -94
  46. package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
  47. package/dist/src/analyze/extract/vue-tsx.js +0 -498
  48. package/dist/src/analyze/extract/vue.d.ts +0 -5
  49. package/dist/src/analyze/extract/vue.js +0 -653
  50. package/dist/src/analyze/extract/web-components.d.ts +0 -2
  51. package/dist/src/analyze/extract/web-components.js +0 -876
  52. package/dist/src/analyze/pre-classify.d.ts +0 -17
  53. package/dist/src/analyze/pre-classify.js +0 -211
  54. package/dist/src/apply/error-parser.d.ts +0 -43
  55. package/dist/src/apply/error-parser.js +0 -163
@@ -1,289 +0,0 @@
1
- import { basename } from 'node:path';
2
- import { readFile } from 'node:fs/promises';
3
- import os from 'node:os';
4
- import { Project, Node } from 'ts-morph';
5
- function extractAllowedValues(typeText) {
6
- // Check if the type is a union of string literals like 'a' | 'b' | 'c'
7
- const parts = typeText.split('|').map((p) => p.trim());
8
- const literals = parts.filter((p) => /^['"]/.test(p)).map((p) => p.replace(/^['"]|['"]$/g, ''));
9
- return literals.length >= 2 ? literals.sort() : undefined;
10
- }
11
- function usesAstroProps(initializer) {
12
- if (!initializer)
13
- return false;
14
- if (initializer.getText() === 'Astro.props')
15
- return true;
16
- let found = false;
17
- initializer.forEachDescendant((node) => {
18
- if (found)
19
- return false;
20
- if (Node.isPropertyAccessExpression(node) && node.getText() === 'Astro.props') {
21
- found = true;
22
- return false;
23
- }
24
- return undefined;
25
- });
26
- return found;
27
- }
28
- function extractBindingPropName(element) {
29
- if (element.getText().startsWith('...'))
30
- return null;
31
- return element.getPropertyNameNode()?.getText() ?? element.getNameNode().getText();
32
- }
33
- function extractFallbackPropsFromFrontmatter(frontmatter) {
34
- const project = new Project({
35
- compilerOptions: {
36
- strict: false,
37
- target: 99,
38
- module: 99,
39
- allowJs: true,
40
- },
41
- useInMemoryFileSystem: true,
42
- skipAddingFilesFromTsConfig: true,
43
- });
44
- const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
45
- const props = new Map();
46
- sf.forEachDescendant((node) => {
47
- if (!Node.isVariableDeclaration(node))
48
- return;
49
- const initializer = node.getInitializer();
50
- if (!usesAstroProps(initializer))
51
- return;
52
- const nameNode = node.getNameNode();
53
- if (!Node.isObjectBindingPattern(nameNode))
54
- return;
55
- for (const element of nameNode.getElements()) {
56
- const propName = extractBindingPropName(element);
57
- if (!propName)
58
- continue;
59
- props.set(propName, {
60
- name: propName,
61
- type: 'any',
62
- required: !element.getInitializer(),
63
- });
64
- }
65
- });
66
- return [...props.values()].sort((a, b) => a.name.localeCompare(b.name));
67
- }
68
- function mergeProps(...propGroups) {
69
- const merged = new Map();
70
- for (const props of propGroups) {
71
- for (const prop of props) {
72
- const existing = merged.get(prop.name);
73
- merged.set(prop.name, existing
74
- ? {
75
- ...existing,
76
- ...prop,
77
- required: existing.required && prop.required,
78
- }
79
- : prop);
80
- }
81
- }
82
- return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
83
- }
84
- function extractPropsFromFrontmatter(frontmatter) {
85
- const project = new Project({
86
- compilerOptions: {
87
- strict: false,
88
- target: 99, // ESNext
89
- module: 99, // ESNext
90
- allowJs: true,
91
- },
92
- useInMemoryFileSystem: true,
93
- skipAddingFilesFromTsConfig: true,
94
- });
95
- const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
96
- const props = [];
97
- // Find `interface Props` or `type Props = ...`
98
- const propsInterface = sf.getInterface('Props');
99
- const propsTypeAlias = sf.getTypeAlias('Props');
100
- if (propsInterface) {
101
- for (const member of propsInterface.getProperties()) {
102
- const name = member.getName();
103
- const typeText = member.getTypeNode()?.getText() ?? 'any';
104
- const required = !member.hasQuestionToken();
105
- const allowedValues = extractAllowedValues(typeText);
106
- props.push({
107
- name,
108
- type: typeText,
109
- required,
110
- ...(allowedValues && { allowedValues }),
111
- sourceStartLine: member.getStartLineNumber(),
112
- sourceEndLine: member.getEndLineNumber(),
113
- });
114
- }
115
- }
116
- else if (propsTypeAlias) {
117
- const type = propsTypeAlias.getType();
118
- for (const property of type.getProperties()) {
119
- const name = property.getName();
120
- const decl = property.getValueDeclaration() ?? property.getDeclarations()[0];
121
- if (!decl)
122
- continue;
123
- const propType = property.getTypeAtLocation(decl);
124
- const typeText = propType.getText(decl);
125
- const required = !property.isOptional();
126
- const allowedValues = extractAllowedValues(typeText);
127
- props.push({
128
- name,
129
- type: typeText,
130
- required,
131
- ...(allowedValues && { allowedValues }),
132
- ...(typeof decl.getStartLineNumber === 'function'
133
- ? {
134
- sourceStartLine: decl.getStartLineNumber(),
135
- sourceEndLine: decl.getEndLineNumber(),
136
- }
137
- : {}),
138
- });
139
- }
140
- }
141
- return props.sort((a, b) => a.name.localeCompare(b.name));
142
- }
143
- function extractDefaultsFromFrontmatter(frontmatter) {
144
- const defaults = new Map();
145
- const project = new Project({
146
- compilerOptions: {
147
- strict: false,
148
- target: 99,
149
- module: 99,
150
- allowJs: true,
151
- },
152
- useInMemoryFileSystem: true,
153
- skipAddingFilesFromTsConfig: true,
154
- });
155
- const sf = project.createSourceFile('__frontmatter__.ts', frontmatter);
156
- sf.forEachDescendant((node) => {
157
- if (!Node.isVariableDeclaration(node))
158
- return;
159
- const initializer = node.getInitializer();
160
- if (!initializer)
161
- return;
162
- if (!usesAstroProps(initializer))
163
- return;
164
- const nameNode = node.getNameNode();
165
- if (!Node.isObjectBindingPattern(nameNode))
166
- return;
167
- for (const element of nameNode.getElements()) {
168
- const propName = extractBindingPropName(element);
169
- if (!propName)
170
- continue;
171
- const elementInitializer = element.getInitializer();
172
- if (!elementInitializer)
173
- continue;
174
- const value = elementInitializer.getText().replace(/^['"]|['"]$/g, '');
175
- defaults.set(propName, value);
176
- }
177
- });
178
- return defaults;
179
- }
180
- function extractSlotsFromTemplate(template) {
181
- const slots = [];
182
- const seen = new Set();
183
- const slotRegex = /<slot(?:\s+name=["']([^"']+)["'])?\s*\/?>/g;
184
- let match;
185
- while ((match = slotRegex.exec(template)) !== null) {
186
- const slotName = match[1] ?? 'default';
187
- if (!seen.has(slotName)) {
188
- seen.add(slotName);
189
- slots.push({ name: slotName, isDefault: slotName === 'default' });
190
- }
191
- }
192
- return slots;
193
- }
194
- function extractSlotsFromFrontmatter(frontmatter) {
195
- const slots = [];
196
- const seen = new Set();
197
- const slotRenderRegex = /Astro\.slots\.render\(\s*['"]([^'"]+)['"]\s*\)/g;
198
- let match;
199
- while ((match = slotRenderRegex.exec(frontmatter)) !== null) {
200
- const slotName = match[1];
201
- if (!seen.has(slotName)) {
202
- seen.add(slotName);
203
- slots.push({ name: slotName, isDefault: slotName === 'default' });
204
- }
205
- }
206
- return slots;
207
- }
208
- function mergeSlots(...slotGroups) {
209
- const merged = [];
210
- const seen = new Set();
211
- for (const slots of slotGroups) {
212
- for (const slot of slots) {
213
- if (seen.has(slot.name))
214
- continue;
215
- seen.add(slot.name);
216
- merged.push(slot);
217
- }
218
- }
219
- return merged;
220
- }
221
- function extractFromAstroFile(filePath, source) {
222
- const name = basename(filePath, '.astro');
223
- // Split on `---` fences: frontmatter is between first and second `---`
224
- // If there is no `---`, the entire file is a template-only component
225
- const fenceIndex = source.startsWith('---') ? 0 : -1;
226
- let frontmatter = '';
227
- let template = source;
228
- if (fenceIndex !== -1) {
229
- const endFenceIndex = source.indexOf('---', fenceIndex + 3);
230
- if (endFenceIndex !== -1) {
231
- frontmatter = source.slice(fenceIndex + 3, endFenceIndex);
232
- template = source.slice(endFenceIndex + 3);
233
- }
234
- }
235
- const props = frontmatter
236
- ? mergeProps(extractFallbackPropsFromFrontmatter(frontmatter), extractPropsFromFrontmatter(frontmatter))
237
- : [];
238
- const defaults = frontmatter ? extractDefaultsFromFrontmatter(frontmatter) : new Map();
239
- const propsWithDefaults = props.map((p) => {
240
- const defaultValue = defaults.get(p.name);
241
- return defaultValue ? { ...p, defaultValue } : p;
242
- });
243
- const slots = mergeSlots(extractSlotsFromTemplate(template), extractSlotsFromFrontmatter(frontmatter));
244
- return {
245
- name,
246
- source: filePath,
247
- sourcePath: filePath,
248
- framework: 'astro',
249
- props: propsWithDefaults,
250
- slots,
251
- };
252
- }
253
- const ASTRO_EXTRACT_CONCURRENCY = Number(process.env['EDS_EXTRACT_CONCURRENCY'] ?? 0) || os.cpus().length;
254
- export async function extractAstroComponents(filePaths, onProgress) {
255
- const astroFiles = filePaths.filter((f) => f.endsWith('.astro'));
256
- if (astroFiles.length === 0) {
257
- return { components: [], warnings: [] };
258
- }
259
- const warnings = [];
260
- const components = [];
261
- let filesProcessed = 0;
262
- let componentsFound = 0;
263
- const queue = [...astroFiles];
264
- async function worker() {
265
- while (queue.length > 0) {
266
- const filePath = queue.shift();
267
- if (!filePath)
268
- break;
269
- try {
270
- const source = await readFile(filePath, 'utf-8');
271
- const component = extractFromAstroFile(filePath, source);
272
- if (component) {
273
- components.push(component);
274
- componentsFound++;
275
- }
276
- }
277
- catch (e) {
278
- warnings.push(`Failed to extract from ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
279
- }
280
- filesProcessed++;
281
- onProgress?.({ filesProcessed, componentsFound });
282
- }
283
- }
284
- await Promise.all(Array.from({ length: Math.min(ASTRO_EXTRACT_CONCURRENCY, astroFiles.length) }, worker));
285
- return {
286
- components: components.sort((a, b) => a.name.localeCompare(b.name)),
287
- warnings,
288
- };
289
- }
@@ -1,16 +0,0 @@
1
- import type { RawComponentDefinition } from '../../types.js';
2
- export interface NonAuthorableResult {
3
- skip: boolean;
4
- reason?: string;
5
- }
6
- /**
7
- * Decides whether a component is non-authorable infrastructure (Context.Provider,
8
- * analytics shim, layout-only utility, etc.) and should be filtered out of the
9
- * analyze TUI before authoring-token generation.
10
- *
11
- * Uses prop-shape signals only — no component-name or source-path patterns.
12
- * A design system can live anywhere under any naming convention; relying on
13
- * suffixes like `*Provider` or paths like `src/lib/` would silently fail in
14
- * other repos.
15
- */
16
- export declare function isNonAuthorableComponent(component: RawComponentDefinition): NonAuthorableResult;
@@ -1,60 +0,0 @@
1
- const HANDLER_TYPE_PATTERN = /=>|EventHandler|Dispatch<|SetStateAction/;
2
- const REF_TYPE_PATTERN = /Ref<|RefObject<|MutableRefObject/;
3
- function isHandlerOrRefProp(prop) {
4
- if (HANDLER_TYPE_PATTERN.test(prop.type))
5
- return true;
6
- if (REF_TYPE_PATTERN.test(prop.type))
7
- return true;
8
- if (/^on[A-Z]/.test(prop.name) || /^set[A-Z]/.test(prop.name))
9
- return true;
10
- if (prop.name === 'ref' || prop.name === 'innerRef')
11
- return true;
12
- return false;
13
- }
14
- /**
15
- * Decides whether a component is non-authorable infrastructure (Context.Provider,
16
- * analytics shim, layout-only utility, etc.) and should be filtered out of the
17
- * analyze TUI before authoring-token generation.
18
- *
19
- * Uses prop-shape signals only — no component-name or source-path patterns.
20
- * A design system can live anywhere under any naming convention; relying on
21
- * suffixes like `*Provider` or paths like `src/lib/` would silently fail in
22
- * other repos.
23
- */
24
- export function isNonAuthorableComponent(component) {
25
- const { props, slots, usesCreateContext } = component;
26
- // R1: zero props AND zero slots — nothing for an editor to author.
27
- // Catches analytics scripts, GTM tags, layout fixers, security tokens, etc.
28
- if (props.length === 0 && slots.length === 0) {
29
- return { skip: true, reason: 'component has no props and no slots' };
30
- }
31
- // R2: createContext source + prop literally named `value` — canonical
32
- // `<Context.Provider value={...}>` call site.
33
- if (usesCreateContext && props.some((p) => p.name === 'value')) {
34
- return {
35
- skip: true,
36
- reason: 'source uses createContext and component exposes a Context.Provider value prop',
37
- };
38
- }
39
- // R3: createContext source + zero props — Provider wrapper that hard-codes
40
- // the context value internally (e.g. FontProvider, BottomSheetProvider).
41
- if (usesCreateContext && props.length === 0) {
42
- return { skip: true, reason: 'source uses createContext and component has no props' };
43
- }
44
- // R4: createContext source + exactly one non-handler prop — Provider that
45
- // takes the context value as its sole data prop, named after the data
46
- // (e.g. `LocaleProvider({ locale })`, `NavigationProvider({ navItems })`).
47
- if (usesCreateContext && props.length === 1 && !isHandlerOrRefProp(props[0])) {
48
- return {
49
- skip: true,
50
- reason: 'source uses createContext and component has a single non-handler prop',
51
- };
52
- }
53
- // R5: every prop is a handler/ref — pure data plumbing, no authoring surface.
54
- // Catches components like `OsanoCookiePlaceholder({ onBannerLoaded })` or
55
- // `FeedbackCard({ setShowModal })`.
56
- if (props.length > 0 && props.every(isHandlerOrRefProp)) {
57
- return { skip: true, reason: 'every prop is a handler or ref' };
58
- }
59
- return { skip: false };
60
- }
@@ -1,6 +0,0 @@
1
- import type { ComponentExtractionResult, ExtractorOptions } from '../../types.js';
2
- export type ExtractProgress = {
3
- filesProcessed: number;
4
- componentsFound: number;
5
- };
6
- export declare function extractComponents(filePaths: string[], onProgress?: (progress: ExtractProgress) => void, opts?: ExtractorOptions): Promise<ComponentExtractionResult>;
@@ -1,314 +0,0 @@
1
- import { extractStencilComponents } from './stencil.js';
2
- import { extractReactComponents } from './react.js';
3
- import { extractVueTsxComponents } from './vue-tsx.js';
4
- import { extractVueComponents } from './vue.js';
5
- import { extractAstroComponents } from './astro.js';
6
- import { extractWebComponentDefinitions } from './web-components.js';
7
- import { extractSvelteComponents } from './svelte.js';
8
- const extractors = [
9
- {
10
- name: 'stencil',
11
- fileFilter: (f) => /\.[jt]sx$/.test(f),
12
- extract: extractStencilComponents,
13
- },
14
- {
15
- name: 'react',
16
- fileFilter: (f) => /\.[jt]sx?$/.test(f) && !f.endsWith('.d.ts'),
17
- extract: extractReactComponents,
18
- },
19
- {
20
- name: 'vue-tsx',
21
- fileFilter: (f) => /\.[jt]sx?$/.test(f) && !f.endsWith('.d.ts'),
22
- extract: extractVueTsxComponents,
23
- },
24
- {
25
- name: 'vue',
26
- fileFilter: (f) => f.endsWith('.vue'),
27
- extract: extractVueComponents,
28
- },
29
- {
30
- name: 'astro',
31
- fileFilter: (f) => f.endsWith('.astro'),
32
- extract: extractAstroComponents,
33
- },
34
- {
35
- name: 'web-components',
36
- fileFilter: (f) => /\.[jt]s$/.test(f) && !/\.[jt]sx$/.test(f) && !f.endsWith('.d.ts'),
37
- extract: extractWebComponentDefinitions,
38
- },
39
- {
40
- name: 'svelte',
41
- fileFilter: (f) => {
42
- if (!f.endsWith('.svelte'))
43
- return false;
44
- // SvelteKit route conventions: +page.svelte, +layout.svelte, +error.svelte
45
- // are framework-managed entrypoints, not authorable design-system components.
46
- // Mirrors how Next.js page.tsx / layout.tsx are filtered in pre-classify.
47
- const filename = f.replace(/\\/g, '/').split('/').pop() ?? '';
48
- if (/^\+(page|layout|error)\.svelte$/.test(filename))
49
- return false;
50
- return true;
51
- },
52
- extract: extractSvelteComponents,
53
- },
54
- ];
55
- function getPathPreferenceScore(filePath) {
56
- const normalized = filePath.replace(/\\/g, '/');
57
- const segments = normalized.split('/').filter(Boolean);
58
- const filename = segments.at(-1) ?? '';
59
- const basename = filename.replace(/\.[^.]+$/, '');
60
- let score = 0;
61
- if (/^index\.[jt]sx?$/.test(filename))
62
- score += 100;
63
- if (basename && segments.at(-2) === basename)
64
- score -= 10;
65
- const componentsSegmentCount = segments.filter((segment) => segment === 'components').length;
66
- score -= componentsSegmentCount * 8;
67
- score -= segments.length;
68
- return score;
69
- }
70
- function getPackageRootInfo(segments) {
71
- for (let index = 0; index < segments.length; index += 1) {
72
- const segment = segments[index];
73
- if ((segment === '.packages' || segment === 'packages') && segments[index + 1]) {
74
- return {
75
- rootSegments: segments.slice(0, index + 2),
76
- relativeSegments: segments.slice(index + 2),
77
- };
78
- }
79
- }
80
- const srcIndex = segments.lastIndexOf('src');
81
- if (srcIndex >= 0) {
82
- return {
83
- rootSegments: segments.slice(0, srcIndex),
84
- relativeSegments: segments.slice(srcIndex),
85
- };
86
- }
87
- return null;
88
- }
89
- function getScopeInfo(filePath) {
90
- const normalized = filePath.replace(/\\/g, '/');
91
- const segments = normalized.split('/').filter(Boolean);
92
- const rootInfo = getPackageRootInfo(segments);
93
- if (!rootInfo) {
94
- return null;
95
- }
96
- const { rootSegments, relativeSegments } = rootInfo;
97
- return {
98
- rootKey: rootSegments.join('/'),
99
- relativeSegments,
100
- };
101
- }
102
- function getTopLevelFamilyName(relativeSegments) {
103
- const [first, second, third] = relativeSegments;
104
- if (relativeSegments.length === 1 && first) {
105
- return first.replace(/\.[^.]+$/, '');
106
- }
107
- if (first === 'src' && second === 'components' && third) {
108
- const fileSegment = relativeSegments[3];
109
- if (fileSegment &&
110
- (fileSegment === `${third}.tsx` ||
111
- fileSegment === `${third}.ts` ||
112
- fileSegment === 'index.tsx' ||
113
- fileSegment === 'index.ts')) {
114
- return third;
115
- }
116
- return null;
117
- }
118
- if (first === 'src' && second) {
119
- const fileSegment = relativeSegments[2];
120
- if (fileSegment &&
121
- (fileSegment === `${second}.vue` ||
122
- fileSegment === `${second}.tsx` ||
123
- fileSegment === `${second}.ts` ||
124
- fileSegment === 'index.tsx' ||
125
- fileSegment === 'index.ts' ||
126
- fileSegment === 'index.vue')) {
127
- return second;
128
- }
129
- return null;
130
- }
131
- if (first) {
132
- const fileSegment = relativeSegments[1];
133
- if (fileSegment === `${first}.tsx` ||
134
- fileSegment === `${first}.ts` ||
135
- fileSegment === `${first}.vue` ||
136
- fileSegment === 'index.tsx' ||
137
- fileSegment === 'index.ts' ||
138
- fileSegment === 'index.vue') {
139
- return first;
140
- }
141
- }
142
- return null;
143
- }
144
- function isWithinTopLevelFamily(relativeSegments, componentName) {
145
- const [first, second, third] = relativeSegments;
146
- const componentFilenames = new Set([
147
- `${componentName}.tsx`,
148
- `${componentName}.ts`,
149
- `${componentName}.vue`,
150
- 'index.tsx',
151
- 'index.ts',
152
- 'index.vue',
153
- ]);
154
- if (relativeSegments.length === 1) {
155
- return new Set([`${componentName}.tsx`, `${componentName}.ts`, `${componentName}.vue`]).has(first);
156
- }
157
- if (first === 'src' && second === 'components' && third === componentName) {
158
- return true;
159
- }
160
- if (first === 'components' && second === componentName) {
161
- return true;
162
- }
163
- if (first === 'src' && second === componentName) {
164
- return true;
165
- }
166
- if (first === componentName) {
167
- return componentFilenames.has(second) || relativeSegments.length > 2;
168
- }
169
- return false;
170
- }
171
- function getFamilyScopeKey(filePath, componentName, topLevelFamiliesByRoot) {
172
- const normalized = filePath.replace(/\\/g, '/');
173
- const scopeInfo = getScopeInfo(filePath);
174
- if (!scopeInfo) {
175
- return normalized;
176
- }
177
- const { rootKey, relativeSegments } = scopeInfo;
178
- const topLevelFamilies = topLevelFamiliesByRoot.get(rootKey);
179
- if (topLevelFamilies?.has(componentName) && isWithinTopLevelFamily(relativeSegments, componentName)) {
180
- return `${rootKey}::${componentName}`;
181
- }
182
- const [first, second, third] = relativeSegments;
183
- if (first === 'src' && second === 'components' && third) {
184
- return `${rootKey}/src/components/${third}`;
185
- }
186
- if (first === 'components' && second) {
187
- return `${rootKey}/components/${second}`;
188
- }
189
- if (first === 'src' && second && relativeSegments.length > 2) {
190
- return `${rootKey}/src/${second}`;
191
- }
192
- if (first && relativeSegments.length > 1) {
193
- return `${rootKey}/${first}`;
194
- }
195
- return rootKey;
196
- }
197
- function choosePreferredComponent(existing, candidate) {
198
- const existingScore = getPathPreferenceScore(existing.source);
199
- const candidateScore = getPathPreferenceScore(candidate.source);
200
- if (candidateScore > existingScore) {
201
- return {
202
- winner: candidate,
203
- loser: existing,
204
- reason: `preferred ${candidate.source} over ${existing.source} based on path heuristics`,
205
- };
206
- }
207
- if (candidateScore < existingScore) {
208
- return {
209
- winner: existing,
210
- loser: candidate,
211
- reason: `kept ${existing.source} over ${candidate.source} based on path heuristics`,
212
- };
213
- }
214
- if (candidate.source.length < existing.source.length) {
215
- return {
216
- winner: candidate,
217
- loser: existing,
218
- reason: `preferred shorter path ${candidate.source} over ${existing.source}`,
219
- };
220
- }
221
- return {
222
- winner: existing,
223
- loser: candidate,
224
- reason: `kept ${existing.source} over ${candidate.source} by stable first-seen order`,
225
- };
226
- }
227
- export async function extractComponents(filePaths, onProgress, opts) {
228
- const filesByExtractor = new Map();
229
- for (const extractor of extractors) {
230
- filesByExtractor.set(extractor, []);
231
- }
232
- for (const filePath of filePaths) {
233
- for (const extractor of extractors) {
234
- if (extractor.fileFilter(filePath)) {
235
- filesByExtractor.get(extractor).push(filePath);
236
- }
237
- }
238
- }
239
- const perExtractorFiles = new Map();
240
- const perExtractorComponents = new Map();
241
- let totalFilesProcessed = 0;
242
- let totalComponentsFound = 0;
243
- const results = await Promise.all(extractors.map(async (extractor) => {
244
- const files = filesByExtractor.get(extractor);
245
- if (files.length === 0)
246
- return { components: [], warnings: [] };
247
- perExtractorFiles.set(extractor, 0);
248
- perExtractorComponents.set(extractor, 0);
249
- const result = await extractor.extract(files, (p) => {
250
- const prevFiles = perExtractorFiles.get(extractor) ?? 0;
251
- const prevComponents = perExtractorComponents.get(extractor) ?? 0;
252
- totalFilesProcessed += p.filesProcessed - prevFiles;
253
- totalComponentsFound += p.componentsFound - prevComponents;
254
- perExtractorFiles.set(extractor, p.filesProcessed);
255
- perExtractorComponents.set(extractor, p.componentsFound);
256
- onProgress?.({ filesProcessed: totalFilesProcessed, componentsFound: totalComponentsFound });
257
- }, opts);
258
- return result;
259
- }));
260
- const allWarnings = [];
261
- const componentsByKey = new Map();
262
- const keysByName = new Map();
263
- const topLevelFamiliesByRoot = new Map();
264
- for (const result of results) {
265
- for (const component of result.components) {
266
- const scopeInfo = getScopeInfo(component.source);
267
- if (!scopeInfo)
268
- continue;
269
- const familyName = getTopLevelFamilyName(scopeInfo.relativeSegments);
270
- if (!familyName)
271
- continue;
272
- const existingFamilies = topLevelFamiliesByRoot.get(scopeInfo.rootKey) ?? new Set();
273
- existingFamilies.add(familyName);
274
- topLevelFamiliesByRoot.set(scopeInfo.rootKey, existingFamilies);
275
- }
276
- }
277
- for (const result of results) {
278
- allWarnings.push(...result.warnings);
279
- for (const component of result.components) {
280
- const scopeKey = getFamilyScopeKey(component.source, component.name, topLevelFamiliesByRoot);
281
- const identityKey = `${component.name}::${scopeKey}`;
282
- const existing = componentsByKey.get(identityKey);
283
- if (existing) {
284
- const selected = choosePreferredComponent(existing, component);
285
- allWarnings.push(`Duplicate component "${component.name}" found in ${component.source} (already seen in ${existing.source}); ${selected.reason}`);
286
- componentsByKey.set(identityKey, selected.winner);
287
- continue;
288
- }
289
- const existingKeys = keysByName.get(component.name) ?? [];
290
- const crossPackageKey = existingKeys.find((key) => key !== identityKey);
291
- if (crossPackageKey) {
292
- const crossPackageComponent = componentsByKey.get(crossPackageKey);
293
- if (crossPackageComponent) {
294
- allWarnings.push(`Component name collision "${component.name}" found in ${component.source} (also seen in ${crossPackageComponent.source})`);
295
- }
296
- }
297
- componentsByKey.set(identityKey, component);
298
- keysByName.set(component.name, [...existingKeys, identityKey]);
299
- }
300
- }
301
- const allComponents = [...componentsByKey.values()];
302
- const filteredComponents = [];
303
- for (const component of allComponents) {
304
- if (/^use[A-Z]/.test(component.name)) {
305
- allWarnings.push(`Skipped hook: ${component.name} (hooks are not renderable components)`);
306
- continue;
307
- }
308
- filteredComponents.push(component);
309
- }
310
- return {
311
- components: filteredComponents.sort((a, b) => a.name.localeCompare(b.name)),
312
- warnings: allWarnings,
313
- };
314
- }
@@ -1,2 +0,0 @@
1
- import type { ComponentExtractionResult } from '../../types.js';
2
- export declare function extractReactComponents(filePaths: string[]): Promise<ComponentExtractionResult>;