@fragments-sdk/cli 0.7.0 → 0.7.2
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/LICENSE +77 -14
- package/dist/bin.js +247 -247
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CVXKXVOY.js → chunk-3T6QL7IY.js} +47 -29
- package/dist/chunk-3T6QL7IY.js.map +1 -0
- package/dist/{chunk-7OPWMLOE.js → chunk-7KUSBMI4.js} +114 -112
- package/dist/chunk-7KUSBMI4.js.map +1 -0
- package/dist/{chunk-XHUDJNN3.js → chunk-DH4ETVSM.js} +18 -18
- package/dist/chunk-DH4ETVSM.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-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-TJ34N7C7.js → chunk-OOGTG5FM.js} +34 -33
- package/dist/chunk-OOGTG5FM.js.map +1 -0
- package/dist/{core-W2HYIQW6.js → core-UQXZTBFZ.js} +24 -26
- package/dist/{generate-LMTISDIJ.js → generate-GP6ZLAQB.js} +5 -5
- package/dist/generate-GP6ZLAQB.js.map +1 -0
- package/dist/index.d.ts +23 -27
- package/dist/index.js +10 -10
- package/dist/{init-7CHRKQ7P.js → init-W72WBSU2.js} +5 -5
- package/dist/{init-7CHRKQ7P.js.map → init-W72WBSU2.js.map} +1 -1
- package/dist/mcp-bin.js +73 -73
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-V54HWRDY.js +12 -0
- package/dist/{service-T2L7VLTE.js → service-PVGTYUKX.js} +6 -6
- package/dist/{static-viewer-GBR7YNF3.js → static-viewer-KILKIVN7.js} +4 -4
- package/dist/{test-OJRXNDO2.js → test-3YRYQRGV.js} +19 -19
- package/dist/test-3YRYQRGV.js.map +1 -0
- package/dist/{tokens-3BWDESVM.js → tokens-IXSQHPQK.js} +5 -5
- package/dist/{viewer-SUFOISZM.js → viewer-K42REJU2.js} +199 -199
- package/dist/viewer-K42REJU2.js.map +1 -0
- package/package.json +13 -2
- package/src/ai.ts +5 -5
- package/src/analyze.ts +11 -11
- package/src/bin.ts +1 -1
- package/src/build.ts +37 -35
- 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 +5 -5
- 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 +19 -19
- 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 +77 -77
- package/src/core/graph-extractor.ts +32 -32
- package/src/core/importAnalyzer.ts +1 -1
- package/src/core/index.ts +22 -23
- package/src/core/loader.ts +21 -24
- package/src/core/node.ts +5 -5
- package/src/core/parser.ts +71 -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/token-parser.ts +9 -1
- 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 +79 -79
- 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/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/LeftSidebar.tsx +28 -28
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +51 -51
- 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/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- 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/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- 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-7OPWMLOE.js.map +0 -1
- package/dist/chunk-CVXKXVOY.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-TJ34N7C7.js.map +0 -1
- package/dist/chunk-XHUDJNN3.js.map +0 -1
- package/dist/generate-LMTISDIJ.js.map +0 -1
- package/dist/scan-WY23TJCP.js +0 -12
- package/dist/test-OJRXNDO2.js.map +0 -1
- package/dist/viewer-SUFOISZM.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-W2HYIQW6.js.map → core-UQXZTBFZ.js.map} +0 -0
- /package/dist/{scan-WY23TJCP.js.map → scan-V54HWRDY.js.map} +0 -0
- /package/dist/{service-T2L7VLTE.js.map → service-PVGTYUKX.js.map} +0 -0
- /package/dist/{static-viewer-GBR7YNF3.js.map → static-viewer-KILKIVN7.js.map} +0 -0
- /package/dist/{tokens-3BWDESVM.js.map → tokens-IXSQHPQK.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import type {
|
|
2
|
+
import type { CompiledFragment, CompiledBlock } from './types.js';
|
|
3
3
|
import {
|
|
4
4
|
buildComponentGraph,
|
|
5
5
|
extractJsxUsageEdges,
|
|
@@ -16,7 +16,7 @@ import { EDGE_TYPE_WEIGHTS } from '@fragments-sdk/context/graph';
|
|
|
16
16
|
// Test helpers
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
|
|
19
|
-
function
|
|
19
|
+
function makeFragment(name: string, overrides: Partial<CompiledFragment> = {}): CompiledFragment {
|
|
20
20
|
return {
|
|
21
21
|
filePath: `src/components/${name}/${name}.fragment.tsx`,
|
|
22
22
|
meta: {
|
|
@@ -58,8 +58,8 @@ function makeBlock(name: string, components: string[], category = 'test'): Compi
|
|
|
58
58
|
|
|
59
59
|
describe('extractJsxUsageEdges()', () => {
|
|
60
60
|
it('detects JSX component usage in variant code', () => {
|
|
61
|
-
const
|
|
62
|
-
Dialog:
|
|
61
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
62
|
+
Dialog: makeFragment('Dialog', {
|
|
63
63
|
variants: [
|
|
64
64
|
{
|
|
65
65
|
name: 'Default',
|
|
@@ -68,10 +68,10 @@ describe('extractJsxUsageEdges()', () => {
|
|
|
68
68
|
},
|
|
69
69
|
],
|
|
70
70
|
}),
|
|
71
|
-
Button:
|
|
71
|
+
Button: makeFragment('Button'),
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
const edges = extractJsxUsageEdges(
|
|
74
|
+
const edges = extractJsxUsageEdges(fragments, new Set(['Dialog', 'Button']));
|
|
75
75
|
expect(edges.length).toBe(1);
|
|
76
76
|
expect(edges[0].source).toBe('Dialog');
|
|
77
77
|
expect(edges[0].target).toBe('Button');
|
|
@@ -79,8 +79,8 @@ describe('extractJsxUsageEdges()', () => {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it('handles compound component notation (Header.Nav)', () => {
|
|
82
|
-
const
|
|
83
|
-
AppShell:
|
|
82
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
83
|
+
AppShell: makeFragment('AppShell', {
|
|
84
84
|
variants: [
|
|
85
85
|
{
|
|
86
86
|
name: 'Default',
|
|
@@ -89,12 +89,12 @@ describe('extractJsxUsageEdges()', () => {
|
|
|
89
89
|
},
|
|
90
90
|
],
|
|
91
91
|
}),
|
|
92
|
-
Header:
|
|
93
|
-
Sidebar:
|
|
92
|
+
Header: makeFragment('Header'),
|
|
93
|
+
Sidebar: makeFragment('Sidebar'),
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
const edges = extractJsxUsageEdges(
|
|
97
|
-
|
|
97
|
+
fragments,
|
|
98
98
|
new Set(['AppShell', 'Header', 'Sidebar']),
|
|
99
99
|
);
|
|
100
100
|
expect(edges.length).toBe(2);
|
|
@@ -103,8 +103,8 @@ describe('extractJsxUsageEdges()', () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it('does not create self-referencing edges', () => {
|
|
106
|
-
const
|
|
107
|
-
Button:
|
|
106
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
107
|
+
Button: makeFragment('Button', {
|
|
108
108
|
variants: [
|
|
109
109
|
{
|
|
110
110
|
name: 'Default',
|
|
@@ -115,49 +115,49 @@ describe('extractJsxUsageEdges()', () => {
|
|
|
115
115
|
}),
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
const edges = extractJsxUsageEdges(
|
|
118
|
+
const edges = extractJsxUsageEdges(fragments, new Set(['Button']));
|
|
119
119
|
expect(edges.length).toBe(0);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
it('deduplicates across variants', () => {
|
|
123
|
-
const
|
|
124
|
-
Dialog:
|
|
123
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
124
|
+
Dialog: makeFragment('Dialog', {
|
|
125
125
|
variants: [
|
|
126
126
|
{ name: 'V1', description: 'V1', code: '<Dialog><Button>OK</Button></Dialog>' },
|
|
127
127
|
{ name: 'V2', description: 'V2', code: '<Dialog><Button>Cancel</Button></Dialog>' },
|
|
128
128
|
],
|
|
129
129
|
}),
|
|
130
|
-
Button:
|
|
130
|
+
Button: makeFragment('Button'),
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
const edges = extractJsxUsageEdges(
|
|
133
|
+
const edges = extractJsxUsageEdges(fragments, new Set(['Dialog', 'Button']));
|
|
134
134
|
// Should only create one edge even though Button appears in two variants
|
|
135
135
|
expect(edges.length).toBe(1);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
it('skips variants without code', () => {
|
|
139
|
-
const
|
|
140
|
-
Dialog:
|
|
139
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
140
|
+
Dialog: makeFragment('Dialog', {
|
|
141
141
|
variants: [
|
|
142
142
|
{ name: 'NoCode', description: 'No code variant' },
|
|
143
143
|
],
|
|
144
144
|
}),
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
-
const edges = extractJsxUsageEdges(
|
|
147
|
+
const edges = extractJsxUsageEdges(fragments, new Set(['Dialog']));
|
|
148
148
|
expect(edges.length).toBe(0);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it('ignores unknown component tags', () => {
|
|
152
|
-
const
|
|
153
|
-
Dialog:
|
|
152
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
153
|
+
Dialog: makeFragment('Dialog', {
|
|
154
154
|
variants: [
|
|
155
155
|
{ name: 'Default', description: 'Default', code: '<Dialog><UnknownThing /></Dialog>' },
|
|
156
156
|
],
|
|
157
157
|
}),
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
const edges = extractJsxUsageEdges(
|
|
160
|
+
const edges = extractJsxUsageEdges(fragments, new Set(['Dialog']));
|
|
161
161
|
expect(edges.length).toBe(0);
|
|
162
162
|
});
|
|
163
163
|
});
|
|
@@ -215,15 +215,15 @@ describe('extractBlockEdges()', () => {
|
|
|
215
215
|
|
|
216
216
|
describe('extractRelationEdges()', () => {
|
|
217
217
|
it('maps parent relation correctly', () => {
|
|
218
|
-
const
|
|
219
|
-
Button:
|
|
218
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
219
|
+
Button: makeFragment('Button', {
|
|
220
220
|
relations: [
|
|
221
221
|
{ component: 'Header', relationship: 'parent', note: 'Used in Header actions' },
|
|
222
222
|
],
|
|
223
223
|
}),
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
-
const edges = extractRelationEdges(
|
|
226
|
+
const edges = extractRelationEdges(fragments);
|
|
227
227
|
expect(edges.length).toBe(1);
|
|
228
228
|
// parent relation: Header is parent of Button → source=Header, target=Button
|
|
229
229
|
expect(edges[0].source).toBe('Header');
|
|
@@ -232,15 +232,15 @@ describe('extractRelationEdges()', () => {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
it('maps child relation correctly', () => {
|
|
235
|
-
const
|
|
236
|
-
Header:
|
|
235
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
236
|
+
Header: makeFragment('Header', {
|
|
237
237
|
relations: [
|
|
238
238
|
{ component: 'Button', relationship: 'child', note: 'Renders action buttons' },
|
|
239
239
|
],
|
|
240
240
|
}),
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
-
const edges = extractRelationEdges(
|
|
243
|
+
const edges = extractRelationEdges(fragments);
|
|
244
244
|
expect(edges.length).toBe(1);
|
|
245
245
|
expect(edges[0].source).toBe('Header');
|
|
246
246
|
expect(edges[0].target).toBe('Button');
|
|
@@ -248,64 +248,64 @@ describe('extractRelationEdges()', () => {
|
|
|
248
248
|
});
|
|
249
249
|
|
|
250
250
|
it('maps alternative relation', () => {
|
|
251
|
-
const
|
|
252
|
-
Dialog:
|
|
251
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
252
|
+
Dialog: makeFragment('Dialog', {
|
|
253
253
|
relations: [
|
|
254
254
|
{ component: 'Popover', relationship: 'alternative', note: 'For non-modal overlays' },
|
|
255
255
|
],
|
|
256
256
|
}),
|
|
257
257
|
};
|
|
258
258
|
|
|
259
|
-
const edges = extractRelationEdges(
|
|
259
|
+
const edges = extractRelationEdges(fragments);
|
|
260
260
|
expect(edges[0].type).toBe('alternative-to');
|
|
261
261
|
expect(edges[0].note).toBe('For non-modal overlays');
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
it('maps sibling relation', () => {
|
|
265
|
-
const
|
|
266
|
-
Header:
|
|
265
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
266
|
+
Header: makeFragment('Header', {
|
|
267
267
|
relations: [
|
|
268
268
|
{ component: 'Sidebar', relationship: 'sibling', note: 'Both in AppShell' },
|
|
269
269
|
],
|
|
270
270
|
}),
|
|
271
271
|
};
|
|
272
272
|
|
|
273
|
-
const edges = extractRelationEdges(
|
|
273
|
+
const edges = extractRelationEdges(fragments);
|
|
274
274
|
expect(edges[0].type).toBe('sibling-of');
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
it('maps composition relation', () => {
|
|
278
|
-
const
|
|
279
|
-
Input:
|
|
278
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
279
|
+
Input: makeFragment('Input', {
|
|
280
280
|
relations: [
|
|
281
281
|
{ component: 'Button', relationship: 'composition', note: 'Often paired in forms' },
|
|
282
282
|
],
|
|
283
283
|
}),
|
|
284
284
|
};
|
|
285
285
|
|
|
286
|
-
const edges = extractRelationEdges(
|
|
286
|
+
const edges = extractRelationEdges(fragments);
|
|
287
287
|
expect(edges[0].type).toBe('composes');
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
-
it('skips
|
|
291
|
-
const
|
|
292
|
-
Button:
|
|
290
|
+
it('skips fragments without relations', () => {
|
|
291
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
292
|
+
Button: makeFragment('Button'),
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
-
const edges = extractRelationEdges(
|
|
295
|
+
const edges = extractRelationEdges(fragments);
|
|
296
296
|
expect(edges.length).toBe(0);
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
it('skips unknown relationship types', () => {
|
|
300
|
-
const
|
|
301
|
-
Button:
|
|
300
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
301
|
+
Button: makeFragment('Button', {
|
|
302
302
|
relations: [
|
|
303
303
|
{ component: 'Icon', relationship: 'unknown-type' as any, note: 'Custom relation' },
|
|
304
304
|
],
|
|
305
305
|
}),
|
|
306
306
|
};
|
|
307
307
|
|
|
308
|
-
const edges = extractRelationEdges(
|
|
308
|
+
const edges = extractRelationEdges(fragments);
|
|
309
309
|
expect(edges.length).toBe(0);
|
|
310
310
|
});
|
|
311
311
|
});
|
|
@@ -316,8 +316,8 @@ describe('extractRelationEdges()', () => {
|
|
|
316
316
|
|
|
317
317
|
describe('inferRequiredChildren()', () => {
|
|
318
318
|
it('identifies sub-components present in ALL variants', () => {
|
|
319
|
-
const
|
|
320
|
-
Dialog:
|
|
319
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
320
|
+
Dialog: makeFragment('Dialog', {
|
|
321
321
|
variants: [
|
|
322
322
|
{ name: 'V1', description: 'V1', code: '<Dialog><Dialog.Content>...</Dialog.Content><Dialog.Footer>...</Dialog.Footer></Dialog>' },
|
|
323
323
|
{ name: 'V2', description: 'V2', code: '<Dialog><Dialog.Content>...</Dialog.Content></Dialog>' },
|
|
@@ -326,26 +326,26 @@ describe('inferRequiredChildren()', () => {
|
|
|
326
326
|
};
|
|
327
327
|
|
|
328
328
|
const autoDetected = new Map([['Dialog', { subComponents: ['Content', 'Footer'] }]]);
|
|
329
|
-
const result = inferRequiredChildren(
|
|
329
|
+
const result = inferRequiredChildren(fragments, autoDetected);
|
|
330
330
|
|
|
331
331
|
expect(result.get('Dialog')).toEqual(['Content']);
|
|
332
332
|
});
|
|
333
333
|
|
|
334
334
|
it('returns empty for components with no subs', () => {
|
|
335
|
-
const
|
|
336
|
-
Button:
|
|
335
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
336
|
+
Button: makeFragment('Button', {
|
|
337
337
|
variants: [{ name: 'Default', description: 'Default', code: '<Button>Click</Button>' }],
|
|
338
338
|
}),
|
|
339
339
|
};
|
|
340
340
|
|
|
341
341
|
const autoDetected = new Map<string, any>();
|
|
342
|
-
const result = inferRequiredChildren(
|
|
342
|
+
const result = inferRequiredChildren(fragments, autoDetected);
|
|
343
343
|
expect(result.size).toBe(0);
|
|
344
344
|
});
|
|
345
345
|
|
|
346
346
|
it('uses ai.subComponents when autoDetected is empty', () => {
|
|
347
|
-
const
|
|
348
|
-
Dialog:
|
|
347
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
348
|
+
Dialog: makeFragment('Dialog', {
|
|
349
349
|
ai: { subComponents: ['Content', 'Footer'] },
|
|
350
350
|
variants: [
|
|
351
351
|
{ name: 'V1', description: 'V1', code: '<Dialog><Dialog.Content>...</Dialog.Content><Dialog.Footer>...</Dialog.Footer></Dialog>' },
|
|
@@ -355,7 +355,7 @@ describe('inferRequiredChildren()', () => {
|
|
|
355
355
|
};
|
|
356
356
|
|
|
357
357
|
const autoDetected = new Map<string, any>();
|
|
358
|
-
const result = inferRequiredChildren(
|
|
358
|
+
const result = inferRequiredChildren(fragments, autoDetected);
|
|
359
359
|
expect(result.get('Dialog')).toEqual(['Content', 'Footer']);
|
|
360
360
|
});
|
|
361
361
|
});
|
|
@@ -366,8 +366,8 @@ describe('inferRequiredChildren()', () => {
|
|
|
366
366
|
|
|
367
367
|
describe('generateCommonPatterns()', () => {
|
|
368
368
|
it('generates skeleton patterns from variant code', () => {
|
|
369
|
-
const
|
|
370
|
-
Dialog:
|
|
369
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
370
|
+
Dialog: makeFragment('Dialog', {
|
|
371
371
|
variants: [
|
|
372
372
|
{
|
|
373
373
|
name: 'Default',
|
|
@@ -379,7 +379,7 @@ describe('generateCommonPatterns()', () => {
|
|
|
379
379
|
};
|
|
380
380
|
|
|
381
381
|
const autoDetected = new Map([['Dialog', { subComponents: ['Content', 'Footer'] }]]);
|
|
382
|
-
const result = generateCommonPatterns(
|
|
382
|
+
const result = generateCommonPatterns(fragments, autoDetected);
|
|
383
383
|
|
|
384
384
|
expect(result.has('Dialog')).toBe(true);
|
|
385
385
|
const patterns = result.get('Dialog')!;
|
|
@@ -389,12 +389,12 @@ describe('generateCommonPatterns()', () => {
|
|
|
389
389
|
});
|
|
390
390
|
|
|
391
391
|
it('returns empty for components without sub-components', () => {
|
|
392
|
-
const
|
|
393
|
-
Button:
|
|
392
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
393
|
+
Button: makeFragment('Button'),
|
|
394
394
|
};
|
|
395
395
|
|
|
396
396
|
const autoDetected = new Map<string, any>();
|
|
397
|
-
const result = generateCommonPatterns(
|
|
397
|
+
const result = generateCommonPatterns(fragments, autoDetected);
|
|
398
398
|
expect(result.size).toBe(0);
|
|
399
399
|
});
|
|
400
400
|
});
|
|
@@ -445,10 +445,10 @@ describe('mergeAndDeduplicate()', () => {
|
|
|
445
445
|
// ---------------------------------------------------------------------------
|
|
446
446
|
|
|
447
447
|
describe('buildComponentGraph()', () => {
|
|
448
|
-
it('builds a graph from
|
|
449
|
-
const
|
|
450
|
-
Button:
|
|
451
|
-
Dialog:
|
|
448
|
+
it('builds a graph from fragments and blocks', async () => {
|
|
449
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
450
|
+
Button: makeFragment('Button'),
|
|
451
|
+
Dialog: makeFragment('Dialog', {
|
|
452
452
|
variants: [
|
|
453
453
|
{ name: 'Default', description: 'Default', code: '<Dialog><Button>OK</Button></Dialog>' },
|
|
454
454
|
],
|
|
@@ -456,15 +456,15 @@ describe('buildComponentGraph()', () => {
|
|
|
456
456
|
{ component: 'Popover', relationship: 'alternative', note: 'Non-modal' },
|
|
457
457
|
],
|
|
458
458
|
}),
|
|
459
|
-
Input:
|
|
460
|
-
Popover:
|
|
459
|
+
Input: makeFragment('Input'),
|
|
460
|
+
Popover: makeFragment('Popover'),
|
|
461
461
|
};
|
|
462
462
|
|
|
463
463
|
const blocks: Record<string, CompiledBlock> = {
|
|
464
464
|
LoginForm: makeBlock('LoginForm', ['Dialog', 'Input', 'Button']),
|
|
465
465
|
};
|
|
466
466
|
|
|
467
|
-
const result = await buildComponentGraph(
|
|
467
|
+
const result = await buildComponentGraph(fragments, blocks, '/tmp/fake', {
|
|
468
468
|
skipSourceAnalysis: true,
|
|
469
469
|
});
|
|
470
470
|
|
|
@@ -487,8 +487,8 @@ describe('buildComponentGraph()', () => {
|
|
|
487
487
|
});
|
|
488
488
|
|
|
489
489
|
it('auto-detects metadata and reports warnings', async () => {
|
|
490
|
-
const
|
|
491
|
-
Dialog:
|
|
490
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
491
|
+
Dialog: makeFragment('Dialog', {
|
|
492
492
|
ai: {
|
|
493
493
|
compositionPattern: 'compound',
|
|
494
494
|
subComponents: ['Content'],
|
|
@@ -500,7 +500,7 @@ describe('buildComponentGraph()', () => {
|
|
|
500
500
|
}),
|
|
501
501
|
};
|
|
502
502
|
|
|
503
|
-
const result = await buildComponentGraph(
|
|
503
|
+
const result = await buildComponentGraph(fragments, {}, '/tmp/fake', {
|
|
504
504
|
skipSourceAnalysis: true,
|
|
505
505
|
});
|
|
506
506
|
|
|
@@ -508,7 +508,7 @@ describe('buildComponentGraph()', () => {
|
|
|
508
508
|
expect(result.graph.nodes.length).toBe(1);
|
|
509
509
|
});
|
|
510
510
|
|
|
511
|
-
it('handles empty
|
|
511
|
+
it('handles empty fragments', async () => {
|
|
512
512
|
const result = await buildComponentGraph({}, {}, '/tmp/fake', {
|
|
513
513
|
skipSourceAnalysis: true,
|
|
514
514
|
});
|
|
@@ -519,17 +519,17 @@ describe('buildComponentGraph()', () => {
|
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
it('computes health metrics', async () => {
|
|
522
|
-
const
|
|
523
|
-
Button:
|
|
524
|
-
Input:
|
|
525
|
-
Orphan:
|
|
522
|
+
const fragments: Record<string, CompiledFragment> = {
|
|
523
|
+
Button: makeFragment('Button'),
|
|
524
|
+
Input: makeFragment('Input'),
|
|
525
|
+
Orphan: makeFragment('Orphan'),
|
|
526
526
|
};
|
|
527
527
|
|
|
528
528
|
const blocks: Record<string, CompiledBlock> = {
|
|
529
529
|
Form: makeBlock('Form', ['Button', 'Input']),
|
|
530
530
|
};
|
|
531
531
|
|
|
532
|
-
const result = await buildComponentGraph(
|
|
532
|
+
const result = await buildComponentGraph(fragments, blocks, '/tmp/fake', {
|
|
533
533
|
skipSourceAnalysis: true,
|
|
534
534
|
});
|
|
535
535
|
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
GraphHealth,
|
|
25
25
|
} from '@fragments-sdk/context/graph';
|
|
26
26
|
import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from '@fragments-sdk/context/graph';
|
|
27
|
-
import type {
|
|
27
|
+
import type { CompiledFragment, CompiledBlock } from './types.js';
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Public API
|
|
@@ -51,15 +51,15 @@ export interface AutoDetectedMetadata {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Build the full ComponentGraph from
|
|
54
|
+
* Build the full ComponentGraph from fragments, blocks, and source code.
|
|
55
55
|
*/
|
|
56
56
|
export async function buildComponentGraph(
|
|
57
|
-
|
|
57
|
+
fragments: Record<string, CompiledFragment>,
|
|
58
58
|
blocks: Record<string, CompiledBlock>,
|
|
59
59
|
componentDir: string,
|
|
60
60
|
options?: GraphBuildOptions,
|
|
61
61
|
): Promise<GraphBuildResult> {
|
|
62
|
-
const knownComponents = new Set(Object.keys(
|
|
62
|
+
const knownComponents = new Set(Object.keys(fragments));
|
|
63
63
|
const allEdges: GraphEdge[] = [];
|
|
64
64
|
const autoDetected = new Map<string, AutoDetectedMetadata>();
|
|
65
65
|
const warnings: string[] = [];
|
|
@@ -80,7 +80,7 @@ export async function buildComponentGraph(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// 2. Extract JSX usage from variant code
|
|
83
|
-
const jsxEdges = extractJsxUsageEdges(
|
|
83
|
+
const jsxEdges = extractJsxUsageEdges(fragments, knownComponents);
|
|
84
84
|
allEdges.push(...jsxEdges);
|
|
85
85
|
|
|
86
86
|
// 3. Extract block co-occurrence edges
|
|
@@ -88,18 +88,18 @@ export async function buildComponentGraph(
|
|
|
88
88
|
allEdges.push(...blockEdges);
|
|
89
89
|
|
|
90
90
|
// 4. Extract relation edges from fragment metadata
|
|
91
|
-
const relationEdges = extractRelationEdges(
|
|
91
|
+
const relationEdges = extractRelationEdges(fragments);
|
|
92
92
|
allEdges.push(...relationEdges);
|
|
93
93
|
|
|
94
94
|
// 5. Infer requiredChildren
|
|
95
|
-
const requiredChildrenMap = inferRequiredChildren(
|
|
95
|
+
const requiredChildrenMap = inferRequiredChildren(fragments, autoDetected);
|
|
96
96
|
for (const [name, children] of requiredChildrenMap) {
|
|
97
97
|
const existing = autoDetected.get(name) ?? {};
|
|
98
98
|
autoDetected.set(name, { ...existing, requiredChildren: children });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// 6. Generate common patterns
|
|
102
|
-
const patternsMap = generateCommonPatterns(
|
|
102
|
+
const patternsMap = generateCommonPatterns(fragments, autoDetected);
|
|
103
103
|
for (const [name, patterns] of patternsMap) {
|
|
104
104
|
const existing = autoDetected.get(name) ?? {};
|
|
105
105
|
autoDetected.set(name, { ...existing, commonPatterns: patterns });
|
|
@@ -109,14 +109,14 @@ export async function buildComponentGraph(
|
|
|
109
109
|
const mergedEdges = mergeAndDeduplicate(allEdges);
|
|
110
110
|
|
|
111
111
|
// 8. Build nodes
|
|
112
|
-
const nodes: ComponentNode[] = Object.entries(
|
|
112
|
+
const nodes: ComponentNode[] = Object.entries(fragments).map(([name, fragment]) => {
|
|
113
113
|
const detected = autoDetected.get(name);
|
|
114
114
|
return {
|
|
115
115
|
name,
|
|
116
|
-
category:
|
|
117
|
-
status:
|
|
118
|
-
compositionPattern:
|
|
119
|
-
subComponents:
|
|
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
120
|
};
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -134,16 +134,16 @@ export async function buildComponentGraph(
|
|
|
134
134
|
const health = computeHealthFromData(nodes, mergedEdges, blockIndex);
|
|
135
135
|
|
|
136
136
|
// 11. Generate drift warnings
|
|
137
|
-
for (const [name,
|
|
137
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
138
138
|
const detected = autoDetected.get(name);
|
|
139
139
|
if (!detected) continue;
|
|
140
140
|
|
|
141
141
|
// Sub-components drift
|
|
142
|
-
if (
|
|
143
|
-
const declared = new Set(
|
|
142
|
+
if (fragment.ai?.subComponents && detected.subComponents) {
|
|
143
|
+
const declared = new Set(fragment.ai.subComponents);
|
|
144
144
|
const found = new Set(detected.subComponents);
|
|
145
145
|
const missing = detected.subComponents.filter(s => !declared.has(s));
|
|
146
|
-
const extra =
|
|
146
|
+
const extra = fragment.ai.subComponents.filter(s => !found.has(s));
|
|
147
147
|
|
|
148
148
|
if (missing.length > 0) {
|
|
149
149
|
warnings.push(
|
|
@@ -339,16 +339,16 @@ export function extractSubComponents(
|
|
|
339
339
|
* Scan variant code strings for JSX element names → renders edges.
|
|
340
340
|
*/
|
|
341
341
|
export function extractJsxUsageEdges(
|
|
342
|
-
|
|
342
|
+
fragments: Record<string, CompiledFragment>,
|
|
343
343
|
knownComponents: Set<string>,
|
|
344
344
|
): GraphEdge[] {
|
|
345
345
|
const edges: GraphEdge[] = [];
|
|
346
346
|
const jsxTagRegex = /<([A-Z][a-zA-Z]*(?:\.[A-Z][a-zA-Z]*)?)/g;
|
|
347
347
|
|
|
348
|
-
for (const [name,
|
|
348
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
349
349
|
const usedComponents = new Set<string>();
|
|
350
350
|
|
|
351
|
-
for (const variant of
|
|
351
|
+
for (const variant of fragment.variants) {
|
|
352
352
|
if (!variant.code) continue;
|
|
353
353
|
|
|
354
354
|
let match: RegExpExecArray | null;
|
|
@@ -410,7 +410,7 @@ export function extractBlockEdges(
|
|
|
410
410
|
* Map fragment ComponentRelation[] to typed graph edges.
|
|
411
411
|
*/
|
|
412
412
|
export function extractRelationEdges(
|
|
413
|
-
|
|
413
|
+
fragments: Record<string, CompiledFragment>,
|
|
414
414
|
): GraphEdge[] {
|
|
415
415
|
const edges: GraphEdge[] = [];
|
|
416
416
|
|
|
@@ -422,10 +422,10 @@ export function extractRelationEdges(
|
|
|
422
422
|
sibling: 'sibling-of',
|
|
423
423
|
};
|
|
424
424
|
|
|
425
|
-
for (const [name,
|
|
426
|
-
if (!
|
|
425
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
426
|
+
if (!fragment.relations) continue;
|
|
427
427
|
|
|
428
|
-
for (const rel of
|
|
428
|
+
for (const rel of fragment.relations) {
|
|
429
429
|
const edgeType = relationToEdgeType[rel.relationship];
|
|
430
430
|
if (!edgeType) continue;
|
|
431
431
|
|
|
@@ -460,17 +460,17 @@ export function extractRelationEdges(
|
|
|
460
460
|
* Infer requiredChildren: sub-components that appear in ALL variant code strings.
|
|
461
461
|
*/
|
|
462
462
|
export function inferRequiredChildren(
|
|
463
|
-
|
|
463
|
+
fragments: Record<string, CompiledFragment>,
|
|
464
464
|
autoDetected: Map<string, AutoDetectedMetadata>,
|
|
465
465
|
): Map<string, string[]> {
|
|
466
466
|
const result = new Map<string, string[]>();
|
|
467
467
|
|
|
468
|
-
for (const [name,
|
|
468
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
469
469
|
const detected = autoDetected.get(name);
|
|
470
|
-
const subs = detected?.subComponents ??
|
|
470
|
+
const subs = detected?.subComponents ?? fragment.ai?.subComponents;
|
|
471
471
|
if (!subs || subs.length === 0) continue;
|
|
472
472
|
|
|
473
|
-
const variantsWithCode =
|
|
473
|
+
const variantsWithCode = fragment.variants.filter(v => v.code);
|
|
474
474
|
if (variantsWithCode.length === 0) continue;
|
|
475
475
|
|
|
476
476
|
const required: string[] = [];
|
|
@@ -499,18 +499,18 @@ export function inferRequiredChildren(
|
|
|
499
499
|
* Generate simplified JSX skeleton patterns from variant code.
|
|
500
500
|
*/
|
|
501
501
|
export function generateCommonPatterns(
|
|
502
|
-
|
|
502
|
+
fragments: Record<string, CompiledFragment>,
|
|
503
503
|
autoDetected: Map<string, AutoDetectedMetadata>,
|
|
504
504
|
): Map<string, string[]> {
|
|
505
505
|
const result = new Map<string, string[]>();
|
|
506
506
|
|
|
507
|
-
for (const [name,
|
|
507
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
508
508
|
const detected = autoDetected.get(name);
|
|
509
|
-
const subs = detected?.subComponents ??
|
|
509
|
+
const subs = detected?.subComponents ?? fragment.ai?.subComponents;
|
|
510
510
|
if (!subs || subs.length === 0) continue;
|
|
511
511
|
|
|
512
512
|
// Build a simplified pattern from the first variant that has code
|
|
513
|
-
const firstVariant =
|
|
513
|
+
const firstVariant = fragment.variants.find(v => v.code);
|
|
514
514
|
if (!firstVariant?.code) continue;
|
|
515
515
|
|
|
516
516
|
// Extract used sub-components from the code
|