@fragments-sdk/cli 0.6.0 → 0.7.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.
Files changed (178) hide show
  1. package/dist/bin.js +529 -285
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-F7ITZPDJ.js → chunk-32VIEOQY.js} +18 -18
  4. package/dist/chunk-32VIEOQY.js.map +1 -0
  5. package/dist/{chunk-SSLQXHNX.js → chunk-5ITIP3ES.js} +27 -27
  6. package/dist/chunk-5ITIP3ES.js.map +1 -0
  7. package/dist/{chunk-RVRTRESS.js → chunk-DQHWLAUV.js} +29 -29
  8. package/dist/chunk-DQHWLAUV.js.map +1 -0
  9. package/dist/{chunk-Q7GOHVOK.js → chunk-GCZMFLDI.js} +67 -32
  10. package/dist/chunk-GCZMFLDI.js.map +1 -0
  11. package/dist/{chunk-6JBGU74P.js → chunk-GHYYFAQN.js} +23 -23
  12. package/dist/chunk-GHYYFAQN.js.map +1 -0
  13. package/dist/{chunk-NWQ4CJOQ.js → chunk-GKX2HPZ6.js} +40 -40
  14. package/dist/chunk-GKX2HPZ6.js.map +1 -0
  15. package/dist/{chunk-D35RGPAG.js → chunk-U6VTHBNI.js} +499 -83
  16. package/dist/chunk-U6VTHBNI.js.map +1 -0
  17. package/dist/{core-SKRPJQZG.js → core-SFHPYR5H.js} +24 -26
  18. package/dist/{generate-7AF7WRVK.js → generate-54GJAWUY.js} +5 -5
  19. package/dist/generate-54GJAWUY.js.map +1 -0
  20. package/dist/index.d.ts +23 -27
  21. package/dist/index.js +10 -10
  22. package/dist/{init-WKGDPYI4.js → init-EIM5WNMP.js} +5 -5
  23. package/dist/{init-WKGDPYI4.js.map → init-EIM5WNMP.js.map} +1 -1
  24. package/dist/mcp-bin.js +73 -73
  25. package/dist/mcp-bin.js.map +1 -1
  26. package/dist/scan-KQBKUS64.js +12 -0
  27. package/dist/{service-F3E4JJM7.js → service-ED2LNCTU.js} +6 -6
  28. package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-Q4F4QP5M.js} +4 -4
  29. package/dist/{test-CJDNJTPZ.js → test-6VN2DA3S.js} +19 -19
  30. package/dist/test-6VN2DA3S.js.map +1 -0
  31. package/dist/{tokens-JAJABYXP.js → tokens-P2B7ZAM3.js} +5 -5
  32. package/dist/{viewer-R3Q6WAMJ.js → viewer-GM7IQPPB.js} +199 -199
  33. package/dist/viewer-GM7IQPPB.js.map +1 -0
  34. package/package.json +2 -2
  35. package/src/ai.ts +5 -5
  36. package/src/analyze.ts +11 -11
  37. package/src/bin.ts +24 -1
  38. package/src/build.ts +64 -21
  39. package/src/commands/a11y.ts +6 -6
  40. package/src/commands/add.ts +11 -11
  41. package/src/commands/audit.ts +4 -4
  42. package/src/commands/baseline.ts +3 -3
  43. package/src/commands/build.ts +8 -8
  44. package/src/commands/compare.ts +20 -20
  45. package/src/commands/context.ts +16 -16
  46. package/src/commands/enhance.ts +36 -36
  47. package/src/commands/generate.ts +1 -1
  48. package/src/commands/graph.ts +274 -0
  49. package/src/commands/init.ts +1 -1
  50. package/src/commands/link/figma.ts +82 -82
  51. package/src/commands/link/index.ts +3 -3
  52. package/src/commands/link/storybook.ts +9 -9
  53. package/src/commands/list.ts +2 -2
  54. package/src/commands/reset.ts +15 -15
  55. package/src/commands/scan.ts +27 -27
  56. package/src/commands/storygen.ts +24 -24
  57. package/src/commands/validate.ts +2 -2
  58. package/src/commands/verify.ts +8 -8
  59. package/src/core/auto-props.ts +4 -4
  60. package/src/core/composition.test.ts +36 -36
  61. package/src/core/composition.ts +83 -20
  62. package/src/core/config.ts +6 -6
  63. package/src/core/{defineSegment.ts → defineFragment.ts} +16 -22
  64. package/src/core/discovery.ts +6 -6
  65. package/src/core/figma.ts +2 -2
  66. package/src/core/graph-extractor.test.ts +542 -0
  67. package/src/core/graph-extractor.ts +601 -0
  68. package/src/core/importAnalyzer.ts +6 -1
  69. package/src/core/index.ts +22 -23
  70. package/src/core/loader.ts +22 -22
  71. package/src/core/node.ts +5 -5
  72. package/src/core/parser.ts +31 -31
  73. package/src/core/previewLoader.ts +1 -1
  74. package/src/core/schema.ts +16 -16
  75. package/src/core/storyAdapter.test.ts +87 -87
  76. package/src/core/storyAdapter.ts +16 -16
  77. package/src/core/types.ts +21 -26
  78. package/src/diff.ts +22 -22
  79. package/src/index.ts +2 -2
  80. package/src/mcp/server.ts +80 -80
  81. package/src/migrate/__tests__/utils/utils.test.ts +3 -3
  82. package/src/migrate/bin.ts +4 -4
  83. package/src/migrate/converter.ts +16 -16
  84. package/src/migrate/index.ts +3 -3
  85. package/src/migrate/migrate.ts +3 -3
  86. package/src/migrate/parser.ts +8 -8
  87. package/src/migrate/report.ts +2 -2
  88. package/src/migrate/types.ts +4 -4
  89. package/src/screenshot.ts +22 -22
  90. package/src/service/__tests__/props-extractor.test.ts +15 -15
  91. package/src/service/analytics.ts +39 -39
  92. package/src/service/enhance/codebase-scanner.ts +1 -1
  93. package/src/service/enhance/index.ts +1 -1
  94. package/src/service/enhance/props-extractor.ts +2 -2
  95. package/src/service/enhance/types.ts +2 -2
  96. package/src/service/index.ts +2 -2
  97. package/src/service/metrics-store.ts +1 -1
  98. package/src/service/patch-generator.ts +1 -1
  99. package/src/setup.ts +52 -52
  100. package/src/shared/dev-server-client.ts +7 -7
  101. package/src/shared/fragment-loader.ts +59 -0
  102. package/src/shared/index.ts +1 -1
  103. package/src/shared/types.ts +4 -4
  104. package/src/static-viewer.ts +35 -35
  105. package/src/test/discovery.ts +6 -6
  106. package/src/test/index.ts +5 -5
  107. package/src/test/reporters/console.ts +1 -1
  108. package/src/test/reporters/junit.ts +1 -1
  109. package/src/test/runner.ts +7 -7
  110. package/src/test/types.ts +3 -3
  111. package/src/test/watch.ts +9 -9
  112. package/src/validators.ts +26 -26
  113. package/src/viewer/__tests__/render-utils.test.ts +28 -28
  114. package/src/viewer/__tests__/viewer-integration.test.ts +4 -4
  115. package/src/viewer/cli/health.ts +26 -26
  116. package/src/viewer/components/App.tsx +201 -103
  117. package/src/viewer/components/BottomPanel.tsx +17 -17
  118. package/src/viewer/components/CodePanel.tsx +3 -3
  119. package/src/viewer/components/CommandPalette.tsx +11 -11
  120. package/src/viewer/components/ComponentGraph.tsx +28 -28
  121. package/src/viewer/components/ComponentHeader.tsx +2 -2
  122. package/src/viewer/components/ContractPanel.tsx +6 -6
  123. package/src/viewer/components/FigmaEmbed.tsx +9 -9
  124. package/src/viewer/components/HealthDashboard.tsx +17 -17
  125. package/src/viewer/components/Icons.tsx +53 -1
  126. package/src/viewer/components/InteractionsPanel.tsx +2 -2
  127. package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
  128. package/src/viewer/components/IsolatedRender.tsx +10 -10
  129. package/src/viewer/components/Layout.tsx +7 -3
  130. package/src/viewer/components/LeftSidebar.tsx +92 -114
  131. package/src/viewer/components/MultiViewportPreview.tsx +14 -14
  132. package/src/viewer/components/PreviewArea.tsx +11 -11
  133. package/src/viewer/components/PreviewFrameHost.tsx +77 -48
  134. package/src/viewer/components/PreviewToolbar.tsx +57 -10
  135. package/src/viewer/components/RightSidebar.tsx +9 -9
  136. package/src/viewer/components/Sidebar.tsx +17 -17
  137. package/src/viewer/components/StoryRenderer.tsx +2 -2
  138. package/src/viewer/components/TokenStylePanel.tsx +1 -1
  139. package/src/viewer/components/UsageSection.tsx +2 -2
  140. package/src/viewer/components/VariantMatrix.tsx +11 -11
  141. package/src/viewer/components/VariantRenderer.tsx +3 -3
  142. package/src/viewer/components/VariantTabs.tsx +2 -2
  143. package/src/viewer/components/ViewportSelector.tsx +56 -45
  144. package/src/viewer/components/_future/CreatePage.tsx +6 -6
  145. package/src/viewer/composition-renderer.ts +11 -11
  146. package/src/viewer/constants/ui.ts +4 -4
  147. package/src/viewer/entry.tsx +40 -40
  148. package/src/viewer/hooks/useFigmaIntegration.ts +1 -1
  149. package/src/viewer/hooks/usePreviewBridge.ts +5 -5
  150. package/src/viewer/hooks/useUrlState.ts +6 -6
  151. package/src/viewer/index.ts +2 -2
  152. package/src/viewer/intelligence/healthReport.ts +17 -17
  153. package/src/viewer/intelligence/styleDrift.ts +1 -1
  154. package/src/viewer/intelligence/usageScanner.ts +1 -1
  155. package/src/viewer/preview-frame.html +22 -13
  156. package/src/viewer/render-template.html +1 -1
  157. package/src/viewer/render-utils.ts +21 -21
  158. package/src/viewer/server.ts +18 -18
  159. package/src/viewer/styles/globals.css +42 -81
  160. package/src/viewer/utils/detectRelationships.ts +22 -22
  161. package/src/viewer/vite-plugin.ts +213 -213
  162. package/dist/chunk-6JBGU74P.js.map +0 -1
  163. package/dist/chunk-D35RGPAG.js.map +0 -1
  164. package/dist/chunk-F7ITZPDJ.js.map +0 -1
  165. package/dist/chunk-NWQ4CJOQ.js.map +0 -1
  166. package/dist/chunk-Q7GOHVOK.js.map +0 -1
  167. package/dist/chunk-RVRTRESS.js.map +0 -1
  168. package/dist/chunk-SSLQXHNX.js.map +0 -1
  169. package/dist/generate-7AF7WRVK.js.map +0 -1
  170. package/dist/scan-K6JNMCGM.js +0 -12
  171. package/dist/test-CJDNJTPZ.js.map +0 -1
  172. package/dist/viewer-R3Q6WAMJ.js.map +0 -1
  173. package/src/shared/segment-loader.ts +0 -59
  174. /package/dist/{core-SKRPJQZG.js.map → core-SFHPYR5H.js.map} +0 -0
  175. /package/dist/{scan-K6JNMCGM.js.map → scan-KQBKUS64.js.map} +0 -0
  176. /package/dist/{service-F3E4JJM7.js.map → service-ED2LNCTU.js.map} +0 -0
  177. /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
  178. /package/dist/{tokens-JAJABYXP.js.map → tokens-P2B7ZAM3.js.map} +0 -0
@@ -0,0 +1,601 @@
1
+ /**
2
+ * Graph extraction pipeline — builds the ComponentGraph from source code,
3
+ * fragment metadata, and block definitions.
4
+ *
5
+ * Absorbs and enhances the logic from importAnalyzer.ts:
6
+ * - Import detection (PascalCase component imports)
7
+ * - Hook dependency detection (useX() calls)
8
+ * - Object.assign sub-component detection
9
+ * - JSX usage in variant code
10
+ * - Block co-occurrence
11
+ * - Fragment relation mapping
12
+ * - Auto-detection of requiredChildren and commonPatterns
13
+ */
14
+
15
+ import ts from 'typescript';
16
+ import { readFileSync, existsSync } from 'node:fs';
17
+ import { resolve, join, basename } from 'node:path';
18
+ import { readdirSync } from 'node:fs';
19
+ import type {
20
+ ComponentGraph,
21
+ ComponentNode,
22
+ GraphEdge,
23
+ GraphEdgeType,
24
+ GraphHealth,
25
+ } from '@fragments-sdk/context/graph';
26
+ import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from '@fragments-sdk/context/graph';
27
+ import type { CompiledFragment, CompiledBlock } from './types.js';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Public API
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export interface GraphBuildOptions {
34
+ /** Skip source-code AST analysis (for testing with mock data) */
35
+ skipSourceAnalysis?: boolean;
36
+ }
37
+
38
+ export interface GraphBuildResult {
39
+ graph: ComponentGraph;
40
+ /** Auto-detected metadata keyed by component name */
41
+ autoDetected: Map<string, AutoDetectedMetadata>;
42
+ /** Warnings about drift between manual declarations and auto-detected values */
43
+ warnings: string[];
44
+ }
45
+
46
+ export interface AutoDetectedMetadata {
47
+ subComponents?: string[];
48
+ compositionPattern?: 'compound' | 'simple' | 'controlled';
49
+ commonPatterns?: string[];
50
+ requiredChildren?: string[];
51
+ }
52
+
53
+ /**
54
+ * Build the full ComponentGraph from fragments, blocks, and source code.
55
+ */
56
+ export async function buildComponentGraph(
57
+ fragments: Record<string, CompiledFragment>,
58
+ blocks: Record<string, CompiledBlock>,
59
+ componentDir: string,
60
+ options?: GraphBuildOptions,
61
+ ): Promise<GraphBuildResult> {
62
+ const knownComponents = new Set(Object.keys(fragments));
63
+ const allEdges: GraphEdge[] = [];
64
+ const autoDetected = new Map<string, AutoDetectedMetadata>();
65
+ const warnings: string[] = [];
66
+
67
+ // 1. Extract edges from source code (imports, hooks, sub-components)
68
+ if (!options?.skipSourceAnalysis) {
69
+ const sourceEdges = extractImportAndHookEdges(componentDir, knownComponents);
70
+ allEdges.push(...sourceEdges);
71
+
72
+ const subComponentResults = extractSubComponents(componentDir, knownComponents);
73
+ for (const [name, subs] of subComponentResults) {
74
+ autoDetected.set(name, {
75
+ ...autoDetected.get(name),
76
+ subComponents: subs,
77
+ compositionPattern: subs.length > 0 ? 'compound' : 'simple',
78
+ });
79
+ }
80
+ }
81
+
82
+ // 2. Extract JSX usage from variant code
83
+ const jsxEdges = extractJsxUsageEdges(fragments, knownComponents);
84
+ allEdges.push(...jsxEdges);
85
+
86
+ // 3. Extract block co-occurrence edges
87
+ const blockEdges = extractBlockEdges(blocks);
88
+ allEdges.push(...blockEdges);
89
+
90
+ // 4. Extract relation edges from fragment metadata
91
+ const relationEdges = extractRelationEdges(fragments);
92
+ allEdges.push(...relationEdges);
93
+
94
+ // 5. Infer requiredChildren
95
+ const requiredChildrenMap = inferRequiredChildren(fragments, autoDetected);
96
+ for (const [name, children] of requiredChildrenMap) {
97
+ const existing = autoDetected.get(name) ?? {};
98
+ autoDetected.set(name, { ...existing, requiredChildren: children });
99
+ }
100
+
101
+ // 6. Generate common patterns
102
+ const patternsMap = generateCommonPatterns(fragments, autoDetected);
103
+ for (const [name, patterns] of patternsMap) {
104
+ const existing = autoDetected.get(name) ?? {};
105
+ autoDetected.set(name, { ...existing, commonPatterns: patterns });
106
+ }
107
+
108
+ // 7. Merge and deduplicate edges
109
+ const mergedEdges = mergeAndDeduplicate(allEdges);
110
+
111
+ // 8. Build nodes
112
+ const nodes: ComponentNode[] = Object.entries(fragments).map(([name, fragment]) => {
113
+ const detected = autoDetected.get(name);
114
+ return {
115
+ name,
116
+ category: fragment.meta.category,
117
+ status: fragment.meta.status ?? 'stable',
118
+ compositionPattern: fragment.ai?.compositionPattern ?? detected?.compositionPattern,
119
+ subComponents: fragment.ai?.subComponents ?? detected?.subComponents,
120
+ };
121
+ });
122
+
123
+ // 9. Build block index for health computation
124
+ const blockIndex = new Map<string, string[]>();
125
+ for (const [blockName, block] of Object.entries(blocks)) {
126
+ for (const comp of block.components) {
127
+ const existing = blockIndex.get(comp);
128
+ if (existing) existing.push(blockName);
129
+ else blockIndex.set(comp, [blockName]);
130
+ }
131
+ }
132
+
133
+ // 10. Compute health
134
+ const health = computeHealthFromData(nodes, mergedEdges, blockIndex);
135
+
136
+ // 11. Generate drift warnings
137
+ for (const [name, fragment] of Object.entries(fragments)) {
138
+ const detected = autoDetected.get(name);
139
+ if (!detected) continue;
140
+
141
+ // Sub-components drift
142
+ if (fragment.ai?.subComponents && detected.subComponents) {
143
+ const declared = new Set(fragment.ai.subComponents);
144
+ const found = new Set(detected.subComponents);
145
+ const missing = detected.subComponents.filter(s => !declared.has(s));
146
+ const extra = fragment.ai.subComponents.filter(s => !found.has(s));
147
+
148
+ if (missing.length > 0) {
149
+ warnings.push(
150
+ `${name}: declares ${declared.size} subComponents but code has ${found.size}. ` +
151
+ `Missing from declaration: ${missing.join(', ')}`
152
+ );
153
+ }
154
+ if (extra.length > 0) {
155
+ warnings.push(
156
+ `${name}: declares subComponents [${extra.join(', ')}] not found in Object.assign`
157
+ );
158
+ }
159
+ }
160
+ }
161
+
162
+ return {
163
+ graph: { nodes, edges: mergedEdges, health },
164
+ autoDetected,
165
+ warnings,
166
+ };
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Extraction functions
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Parse index.tsx files for import declarations and hook calls.
175
+ * Returns import edges and hook-depends edges.
176
+ */
177
+ export function extractImportAndHookEdges(
178
+ componentDir: string,
179
+ knownComponents: Set<string>,
180
+ ): GraphEdge[] {
181
+ const edges: GraphEdge[] = [];
182
+
183
+ for (const componentName of knownComponents) {
184
+ const indexPath = findComponentIndex(componentDir, componentName);
185
+ if (!indexPath) continue;
186
+
187
+ let sourceText: string;
188
+ try {
189
+ sourceText = readFileSync(indexPath, 'utf-8');
190
+ } catch {
191
+ continue;
192
+ }
193
+
194
+ const sourceFile = ts.createSourceFile(
195
+ indexPath,
196
+ sourceText,
197
+ ts.ScriptTarget.Latest,
198
+ true,
199
+ indexPath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
200
+ );
201
+
202
+ // Walk AST for imports and hook calls
203
+ const visitNode = (node: ts.Node) => {
204
+ // Import declarations
205
+ if (ts.isImportDeclaration(node)) {
206
+ const moduleSpecifier = node.moduleSpecifier;
207
+ if (ts.isStringLiteral(moduleSpecifier)) {
208
+ const importPath = moduleSpecifier.text;
209
+ // Only relative imports
210
+ if (importPath.startsWith('.') || importPath.startsWith('/')) {
211
+ const clause = node.importClause;
212
+ if (clause) {
213
+ // Default import
214
+ if (clause.name && isPascalCase(clause.name.text) && knownComponents.has(clause.name.text)) {
215
+ edges.push({
216
+ source: componentName,
217
+ target: clause.name.text,
218
+ type: 'imports',
219
+ weight: EDGE_TYPE_WEIGHTS['imports'],
220
+ provenance: `source:${componentName}/index.tsx`,
221
+ });
222
+ }
223
+ // Named imports
224
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
225
+ for (const element of clause.namedBindings.elements) {
226
+ const name = element.name.text;
227
+ if (isPascalCase(name) && knownComponents.has(name) && name !== componentName) {
228
+ edges.push({
229
+ source: componentName,
230
+ target: name,
231
+ type: 'imports',
232
+ weight: EDGE_TYPE_WEIGHTS['imports'],
233
+ provenance: `source:${componentName}/index.tsx`,
234
+ });
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ // Hook calls: useX() where X is a known component
244
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
245
+ const callName = node.expression.text;
246
+ const hookMatch = callName.match(/^use([A-Z][a-zA-Z]*)$/);
247
+ if (hookMatch) {
248
+ const hookTarget = hookMatch[1];
249
+ if (knownComponents.has(hookTarget) && hookTarget !== componentName) {
250
+ edges.push({
251
+ source: componentName,
252
+ target: hookTarget,
253
+ type: 'hook-depends',
254
+ weight: EDGE_TYPE_WEIGHTS['hook-depends'],
255
+ provenance: `source:${componentName}/index.tsx`,
256
+ });
257
+ }
258
+ }
259
+ }
260
+
261
+ ts.forEachChild(node, visitNode);
262
+ };
263
+
264
+ ts.forEachChild(sourceFile, visitNode);
265
+ }
266
+
267
+ return edges;
268
+ }
269
+
270
+ /**
271
+ * Detect Object.assign(Root, { Sub1, Sub2 }) patterns in index.tsx files.
272
+ * Returns a map of component name → sub-component names.
273
+ */
274
+ export function extractSubComponents(
275
+ componentDir: string,
276
+ knownComponents: Set<string>,
277
+ ): Map<string, string[]> {
278
+ const result = new Map<string, string[]>();
279
+
280
+ for (const componentName of knownComponents) {
281
+ const indexPath = findComponentIndex(componentDir, componentName);
282
+ if (!indexPath) continue;
283
+
284
+ let sourceText: string;
285
+ try {
286
+ sourceText = readFileSync(indexPath, 'utf-8');
287
+ } catch {
288
+ continue;
289
+ }
290
+
291
+ // Quick regex check first for performance
292
+ if (!sourceText.includes('Object.assign')) continue;
293
+
294
+ const sourceFile = ts.createSourceFile(
295
+ indexPath,
296
+ sourceText,
297
+ ts.ScriptTarget.Latest,
298
+ true,
299
+ indexPath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
300
+ );
301
+
302
+ const subComponents: string[] = [];
303
+
304
+ const visitNode = (node: ts.Node) => {
305
+ // Look for Object.assign(X, { A, B, C })
306
+ if (
307
+ ts.isCallExpression(node) &&
308
+ ts.isPropertyAccessExpression(node.expression) &&
309
+ ts.isIdentifier(node.expression.expression) &&
310
+ node.expression.expression.text === 'Object' &&
311
+ node.expression.name.text === 'assign' &&
312
+ node.arguments.length >= 2
313
+ ) {
314
+ const propsArg = node.arguments[1];
315
+ if (ts.isObjectLiteralExpression(propsArg)) {
316
+ for (const prop of propsArg.properties) {
317
+ if (ts.isShorthandPropertyAssignment(prop)) {
318
+ subComponents.push(prop.name.text);
319
+ } else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
320
+ subComponents.push(prop.name.text);
321
+ }
322
+ }
323
+ }
324
+ }
325
+ ts.forEachChild(node, visitNode);
326
+ };
327
+
328
+ ts.forEachChild(sourceFile, visitNode);
329
+
330
+ if (subComponents.length > 0) {
331
+ result.set(componentName, subComponents);
332
+ }
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ /**
339
+ * Scan variant code strings for JSX element names → renders edges.
340
+ */
341
+ export function extractJsxUsageEdges(
342
+ fragments: Record<string, CompiledFragment>,
343
+ knownComponents: Set<string>,
344
+ ): GraphEdge[] {
345
+ const edges: GraphEdge[] = [];
346
+ const jsxTagRegex = /<([A-Z][a-zA-Z]*(?:\.[A-Z][a-zA-Z]*)?)/g;
347
+
348
+ for (const [name, fragment] of Object.entries(fragments)) {
349
+ const usedComponents = new Set<string>();
350
+
351
+ for (const variant of fragment.variants) {
352
+ if (!variant.code) continue;
353
+
354
+ let match: RegExpExecArray | null;
355
+ jsxTagRegex.lastIndex = 0;
356
+ while ((match = jsxTagRegex.exec(variant.code)) !== null) {
357
+ let tagName = match[1];
358
+ // Handle compound: Header.Nav → Header
359
+ if (tagName.includes('.')) {
360
+ tagName = tagName.split('.')[0];
361
+ }
362
+ if (knownComponents.has(tagName) && tagName !== name) {
363
+ usedComponents.add(tagName);
364
+ }
365
+ }
366
+ }
367
+
368
+ for (const target of usedComponents) {
369
+ edges.push({
370
+ source: name,
371
+ target,
372
+ type: 'renders',
373
+ weight: EDGE_TYPE_WEIGHTS['renders'],
374
+ provenance: `variant:${name}`,
375
+ });
376
+ }
377
+ }
378
+
379
+ return edges;
380
+ }
381
+
382
+ /**
383
+ * Create composes edges from block component lists (pairwise).
384
+ */
385
+ export function extractBlockEdges(
386
+ blocks: Record<string, CompiledBlock>,
387
+ ): GraphEdge[] {
388
+ const edges: GraphEdge[] = [];
389
+
390
+ for (const [blockName, block] of Object.entries(blocks)) {
391
+ const components = block.components;
392
+ // Create pairwise edges (avoid duplicates by only going i < j)
393
+ for (let i = 0; i < components.length; i++) {
394
+ for (let j = i + 1; j < components.length; j++) {
395
+ edges.push({
396
+ source: components[i],
397
+ target: components[j],
398
+ type: 'composes',
399
+ weight: EDGE_TYPE_WEIGHTS['composes'],
400
+ provenance: `block:${blockName}`,
401
+ });
402
+ }
403
+ }
404
+ }
405
+
406
+ return edges;
407
+ }
408
+
409
+ /**
410
+ * Map fragment ComponentRelation[] to typed graph edges.
411
+ */
412
+ export function extractRelationEdges(
413
+ fragments: Record<string, CompiledFragment>,
414
+ ): GraphEdge[] {
415
+ const edges: GraphEdge[] = [];
416
+
417
+ const relationToEdgeType: Record<string, GraphEdgeType> = {
418
+ parent: 'parent-of',
419
+ child: 'parent-of', // reversed: if A declares child B, edge is A parent-of B
420
+ composition: 'composes',
421
+ alternative: 'alternative-to',
422
+ sibling: 'sibling-of',
423
+ };
424
+
425
+ for (const [name, fragment] of Object.entries(fragments)) {
426
+ if (!fragment.relations) continue;
427
+
428
+ for (const rel of fragment.relations) {
429
+ const edgeType = relationToEdgeType[rel.relationship];
430
+ if (!edgeType) continue;
431
+
432
+ // For 'child' relation, source is the current component (parent)
433
+ // For 'parent' relation, source is the related component
434
+ let source: string;
435
+ let target: string;
436
+
437
+ if (rel.relationship === 'parent') {
438
+ source = rel.component;
439
+ target = name;
440
+ } else {
441
+ source = name;
442
+ target = rel.component;
443
+ }
444
+
445
+ edges.push({
446
+ source,
447
+ target,
448
+ type: edgeType,
449
+ weight: EDGE_TYPE_WEIGHTS[edgeType],
450
+ note: rel.note,
451
+ provenance: 'relation',
452
+ });
453
+ }
454
+ }
455
+
456
+ return edges;
457
+ }
458
+
459
+ /**
460
+ * Infer requiredChildren: sub-components that appear in ALL variant code strings.
461
+ */
462
+ export function inferRequiredChildren(
463
+ fragments: Record<string, CompiledFragment>,
464
+ autoDetected: Map<string, AutoDetectedMetadata>,
465
+ ): Map<string, string[]> {
466
+ const result = new Map<string, string[]>();
467
+
468
+ for (const [name, fragment] of Object.entries(fragments)) {
469
+ const detected = autoDetected.get(name);
470
+ const subs = detected?.subComponents ?? fragment.ai?.subComponents;
471
+ if (!subs || subs.length === 0) continue;
472
+
473
+ const variantsWithCode = fragment.variants.filter(v => v.code);
474
+ if (variantsWithCode.length === 0) continue;
475
+
476
+ const required: string[] = [];
477
+ for (const sub of subs) {
478
+ // Check if this sub-component appears in ALL variants
479
+ const inAll = variantsWithCode.every(v => {
480
+ // Match <ComponentName.SubName or just <SubName
481
+ const patterns = [
482
+ new RegExp(`<${name}\\.${sub}[\\s/>]`),
483
+ new RegExp(`<${sub}[\\s/>]`),
484
+ ];
485
+ return patterns.some(p => p.test(v.code!));
486
+ });
487
+ if (inAll) required.push(sub);
488
+ }
489
+
490
+ if (required.length > 0) {
491
+ result.set(name, required);
492
+ }
493
+ }
494
+
495
+ return result;
496
+ }
497
+
498
+ /**
499
+ * Generate simplified JSX skeleton patterns from variant code.
500
+ */
501
+ export function generateCommonPatterns(
502
+ fragments: Record<string, CompiledFragment>,
503
+ autoDetected: Map<string, AutoDetectedMetadata>,
504
+ ): Map<string, string[]> {
505
+ const result = new Map<string, string[]>();
506
+
507
+ for (const [name, fragment] of Object.entries(fragments)) {
508
+ const detected = autoDetected.get(name);
509
+ const subs = detected?.subComponents ?? fragment.ai?.subComponents;
510
+ if (!subs || subs.length === 0) continue;
511
+
512
+ // Build a simplified pattern from the first variant that has code
513
+ const firstVariant = fragment.variants.find(v => v.code);
514
+ if (!firstVariant?.code) continue;
515
+
516
+ // Extract used sub-components from the code
517
+ const usedSubs: string[] = [];
518
+ for (const sub of subs) {
519
+ const patterns = [
520
+ new RegExp(`<${name}\\.${sub}`),
521
+ new RegExp(`<${sub}[\\s/>]`),
522
+ ];
523
+ if (patterns.some(p => p.test(firstVariant.code!))) {
524
+ usedSubs.push(sub);
525
+ }
526
+ }
527
+
528
+ if (usedSubs.length > 0) {
529
+ const pattern = `<${name}>\n${usedSubs.map(s => ` <${name}.${s}>...</${name}.${s}>`).join('\n')}\n</${name}>`;
530
+ result.set(name, [pattern]);
531
+ }
532
+ }
533
+
534
+ return result;
535
+ }
536
+
537
+ /**
538
+ * Merge edges: key by (source, target, type), keep highest weight.
539
+ */
540
+ export function mergeAndDeduplicate(edges: GraphEdge[]): GraphEdge[] {
541
+ const edgeMap = new Map<string, GraphEdge>();
542
+
543
+ for (const edge of edges) {
544
+ const key = `${edge.source}→${edge.target}:${edge.type}`;
545
+ const existing = edgeMap.get(key);
546
+ if (!existing || edge.weight > existing.weight) {
547
+ edgeMap.set(key, edge);
548
+ }
549
+ }
550
+
551
+ return [...edgeMap.values()];
552
+ }
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // Helpers
556
+ // ---------------------------------------------------------------------------
557
+
558
+ function isPascalCase(name: string): boolean {
559
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
560
+ }
561
+
562
+ /**
563
+ * Find the index.tsx file for a component in the component directory.
564
+ */
565
+ function findComponentIndex(componentDir: string, componentName: string): string | null {
566
+ // Try direct path: componentDir/ComponentName/index.tsx
567
+ const candidates = [
568
+ join(componentDir, componentName, 'index.tsx'),
569
+ join(componentDir, componentName, 'index.ts'),
570
+ join(componentDir, componentName, `${componentName}.tsx`),
571
+ join(componentDir, componentName, `${componentName}.ts`),
572
+ ];
573
+
574
+ // Also search within subdirectories matching the component structure
575
+ // e.g., src/components/ComponentName/index.tsx
576
+ for (const candidate of candidates) {
577
+ if (existsSync(candidate)) {
578
+ return candidate;
579
+ }
580
+ }
581
+
582
+ // Try scanning componentDir for directories matching the component name
583
+ try {
584
+ const entries = readdirSync(componentDir, { withFileTypes: true });
585
+ for (const entry of entries) {
586
+ if (entry.isDirectory() && entry.name === componentName) {
587
+ const subCandidates = [
588
+ join(componentDir, entry.name, 'index.tsx'),
589
+ join(componentDir, entry.name, 'index.ts'),
590
+ ];
591
+ for (const sc of subCandidates) {
592
+ if (existsSync(sc)) return sc;
593
+ }
594
+ }
595
+ }
596
+ } catch {
597
+ // Directory might not exist
598
+ }
599
+
600
+ return null;
601
+ }
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * Analyzes TypeScript/JavaScript files to find which components import others,
5
5
  * providing accurate "used by" relationships for the component graph.
6
+ *
7
+ * @deprecated Import analysis is now handled by graph-extractor.ts which provides
8
+ * richer edge-typed relationships via the ComponentGraph. These functions are
9
+ * maintained for backward compatibility. For new code, use
10
+ * `buildComponentGraph()` from `./graph-extractor.js` instead.
6
11
  */
7
12
 
8
13
  import ts from 'typescript';
@@ -45,7 +50,7 @@ function extractComponentNameFromPath(filePath: string): string {
45
50
  return fileName
46
51
  .replace(/\.(tsx?|jsx?)$/, '')
47
52
  .replace(/\.stories$/, '')
48
- .replace(/\.segment$/, '')
53
+ .replace(/\.fragment$/, '')
49
54
  .replace(/\.fragment$/, '');
50
55
  }
51
56