@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.
- package/dist/bin.js +529 -285
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-F7ITZPDJ.js → chunk-32VIEOQY.js} +18 -18
- package/dist/chunk-32VIEOQY.js.map +1 -0
- package/dist/{chunk-SSLQXHNX.js → chunk-5ITIP3ES.js} +27 -27
- package/dist/chunk-5ITIP3ES.js.map +1 -0
- package/dist/{chunk-RVRTRESS.js → chunk-DQHWLAUV.js} +29 -29
- package/dist/chunk-DQHWLAUV.js.map +1 -0
- package/dist/{chunk-Q7GOHVOK.js → chunk-GCZMFLDI.js} +67 -32
- package/dist/chunk-GCZMFLDI.js.map +1 -0
- package/dist/{chunk-6JBGU74P.js → chunk-GHYYFAQN.js} +23 -23
- package/dist/chunk-GHYYFAQN.js.map +1 -0
- package/dist/{chunk-NWQ4CJOQ.js → chunk-GKX2HPZ6.js} +40 -40
- package/dist/chunk-GKX2HPZ6.js.map +1 -0
- package/dist/{chunk-D35RGPAG.js → chunk-U6VTHBNI.js} +499 -83
- package/dist/chunk-U6VTHBNI.js.map +1 -0
- package/dist/{core-SKRPJQZG.js → core-SFHPYR5H.js} +24 -26
- package/dist/{generate-7AF7WRVK.js → generate-54GJAWUY.js} +5 -5
- package/dist/generate-54GJAWUY.js.map +1 -0
- package/dist/index.d.ts +23 -27
- package/dist/index.js +10 -10
- package/dist/{init-WKGDPYI4.js → init-EIM5WNMP.js} +5 -5
- package/dist/{init-WKGDPYI4.js.map → init-EIM5WNMP.js.map} +1 -1
- package/dist/mcp-bin.js +73 -73
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-KQBKUS64.js +12 -0
- package/dist/{service-F3E4JJM7.js → service-ED2LNCTU.js} +6 -6
- package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-Q4F4QP5M.js} +4 -4
- package/dist/{test-CJDNJTPZ.js → test-6VN2DA3S.js} +19 -19
- package/dist/test-6VN2DA3S.js.map +1 -0
- package/dist/{tokens-JAJABYXP.js → tokens-P2B7ZAM3.js} +5 -5
- package/dist/{viewer-R3Q6WAMJ.js → viewer-GM7IQPPB.js} +199 -199
- package/dist/viewer-GM7IQPPB.js.map +1 -0
- package/package.json +2 -2
- package/src/ai.ts +5 -5
- package/src/analyze.ts +11 -11
- package/src/bin.ts +24 -1
- package/src/build.ts +64 -21
- package/src/commands/a11y.ts +6 -6
- package/src/commands/add.ts +11 -11
- package/src/commands/audit.ts +4 -4
- package/src/commands/baseline.ts +3 -3
- package/src/commands/build.ts +8 -8
- package/src/commands/compare.ts +20 -20
- package/src/commands/context.ts +16 -16
- package/src/commands/enhance.ts +36 -36
- package/src/commands/generate.ts +1 -1
- package/src/commands/graph.ts +274 -0
- package/src/commands/init.ts +1 -1
- package/src/commands/link/figma.ts +82 -82
- package/src/commands/link/index.ts +3 -3
- package/src/commands/link/storybook.ts +9 -9
- package/src/commands/list.ts +2 -2
- package/src/commands/reset.ts +15 -15
- package/src/commands/scan.ts +27 -27
- package/src/commands/storygen.ts +24 -24
- package/src/commands/validate.ts +2 -2
- package/src/commands/verify.ts +8 -8
- package/src/core/auto-props.ts +4 -4
- package/src/core/composition.test.ts +36 -36
- package/src/core/composition.ts +83 -20
- package/src/core/config.ts +6 -6
- package/src/core/{defineSegment.ts → defineFragment.ts} +16 -22
- package/src/core/discovery.ts +6 -6
- package/src/core/figma.ts +2 -2
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +6 -1
- package/src/core/index.ts +22 -23
- package/src/core/loader.ts +22 -22
- package/src/core/node.ts +5 -5
- package/src/core/parser.ts +31 -31
- package/src/core/previewLoader.ts +1 -1
- package/src/core/schema.ts +16 -16
- package/src/core/storyAdapter.test.ts +87 -87
- package/src/core/storyAdapter.ts +16 -16
- package/src/core/types.ts +21 -26
- package/src/diff.ts +22 -22
- package/src/index.ts +2 -2
- package/src/mcp/server.ts +80 -80
- package/src/migrate/__tests__/utils/utils.test.ts +3 -3
- package/src/migrate/bin.ts +4 -4
- package/src/migrate/converter.ts +16 -16
- package/src/migrate/index.ts +3 -3
- package/src/migrate/migrate.ts +3 -3
- package/src/migrate/parser.ts +8 -8
- package/src/migrate/report.ts +2 -2
- package/src/migrate/types.ts +4 -4
- package/src/screenshot.ts +22 -22
- package/src/service/__tests__/props-extractor.test.ts +15 -15
- package/src/service/analytics.ts +39 -39
- package/src/service/enhance/codebase-scanner.ts +1 -1
- package/src/service/enhance/index.ts +1 -1
- package/src/service/enhance/props-extractor.ts +2 -2
- package/src/service/enhance/types.ts +2 -2
- package/src/service/index.ts +2 -2
- package/src/service/metrics-store.ts +1 -1
- package/src/service/patch-generator.ts +1 -1
- package/src/setup.ts +52 -52
- package/src/shared/dev-server-client.ts +7 -7
- package/src/shared/fragment-loader.ts +59 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/types.ts +4 -4
- package/src/static-viewer.ts +35 -35
- package/src/test/discovery.ts +6 -6
- package/src/test/index.ts +5 -5
- package/src/test/reporters/console.ts +1 -1
- package/src/test/reporters/junit.ts +1 -1
- package/src/test/runner.ts +7 -7
- package/src/test/types.ts +3 -3
- package/src/test/watch.ts +9 -9
- package/src/validators.ts +26 -26
- package/src/viewer/__tests__/render-utils.test.ts +28 -28
- package/src/viewer/__tests__/viewer-integration.test.ts +4 -4
- package/src/viewer/cli/health.ts +26 -26
- package/src/viewer/components/App.tsx +201 -103
- package/src/viewer/components/BottomPanel.tsx +17 -17
- package/src/viewer/components/CodePanel.tsx +3 -3
- package/src/viewer/components/CommandPalette.tsx +11 -11
- package/src/viewer/components/ComponentGraph.tsx +28 -28
- package/src/viewer/components/ComponentHeader.tsx +2 -2
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/FigmaEmbed.tsx +9 -9
- package/src/viewer/components/HealthDashboard.tsx +17 -17
- package/src/viewer/components/Icons.tsx +53 -1
- package/src/viewer/components/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/Layout.tsx +7 -3
- package/src/viewer/components/LeftSidebar.tsx +92 -114
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +77 -48
- package/src/viewer/components/PreviewToolbar.tsx +57 -10
- package/src/viewer/components/RightSidebar.tsx +9 -9
- package/src/viewer/components/Sidebar.tsx +17 -17
- package/src/viewer/components/StoryRenderer.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/UsageSection.tsx +2 -2
- package/src/viewer/components/VariantMatrix.tsx +11 -11
- package/src/viewer/components/VariantRenderer.tsx +3 -3
- package/src/viewer/components/VariantTabs.tsx +2 -2
- package/src/viewer/components/ViewportSelector.tsx +56 -45
- package/src/viewer/components/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- package/src/viewer/constants/ui.ts +4 -4
- package/src/viewer/entry.tsx +40 -40
- package/src/viewer/hooks/useFigmaIntegration.ts +1 -1
- package/src/viewer/hooks/usePreviewBridge.ts +5 -5
- package/src/viewer/hooks/useUrlState.ts +6 -6
- package/src/viewer/index.ts +2 -2
- package/src/viewer/intelligence/healthReport.ts +17 -17
- package/src/viewer/intelligence/styleDrift.ts +1 -1
- package/src/viewer/intelligence/usageScanner.ts +1 -1
- package/src/viewer/preview-frame.html +22 -13
- package/src/viewer/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- package/src/viewer/styles/globals.css +42 -81
- package/src/viewer/utils/detectRelationships.ts +22 -22
- package/src/viewer/vite-plugin.ts +213 -213
- package/dist/chunk-6JBGU74P.js.map +0 -1
- package/dist/chunk-D35RGPAG.js.map +0 -1
- package/dist/chunk-F7ITZPDJ.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-Q7GOHVOK.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-SSLQXHNX.js.map +0 -1
- package/dist/generate-7AF7WRVK.js.map +0 -1
- package/dist/scan-K6JNMCGM.js +0 -12
- package/dist/test-CJDNJTPZ.js.map +0 -1
- package/dist/viewer-R3Q6WAMJ.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-SKRPJQZG.js.map → core-SFHPYR5H.js.map} +0 -0
- /package/dist/{scan-K6JNMCGM.js.map → scan-KQBKUS64.js.map} +0 -0
- /package/dist/{service-F3E4JJM7.js.map → service-ED2LNCTU.js.map} +0 -0
- /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
- /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(/\.
|
|
53
|
+
.replace(/\.fragment$/, '')
|
|
49
54
|
.replace(/\.fragment$/, '');
|
|
50
55
|
}
|
|
51
56
|
|