@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,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { analyzeComposition } from "./composition.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { CompiledFragment } from "./types.js";
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
function makeFragment(overrides: Partial<CompiledFragment> & { meta: CompiledFragment["meta"] }): CompiledFragment {
|
|
6
6
|
return {
|
|
7
7
|
filePath: "src/components/Test.tsx",
|
|
8
8
|
usage: { when: [], whenNot: [] },
|
|
@@ -12,8 +12,8 @@ function makeSegment(overrides: Partial<CompiledSegment> & { meta: CompiledSegme
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
Button:
|
|
15
|
+
const baseFragments: Record<string, CompiledFragment> = {
|
|
16
|
+
Button: makeFragment({
|
|
17
17
|
meta: { name: "Button", description: "A button", category: "actions", status: "stable" },
|
|
18
18
|
relations: [
|
|
19
19
|
{ component: "IconButton", relationship: "alternative", note: "Use for icon-only actions" },
|
|
@@ -24,19 +24,19 @@ const baseSegments: Record<string, CompiledSegment> = {
|
|
|
24
24
|
whenNot: ["Use Link for navigation"],
|
|
25
25
|
},
|
|
26
26
|
}),
|
|
27
|
-
IconButton:
|
|
27
|
+
IconButton: makeFragment({
|
|
28
28
|
meta: { name: "IconButton", description: "Icon-only button", category: "actions", status: "stable" },
|
|
29
29
|
relations: [
|
|
30
30
|
{ component: "Button", relationship: "alternative", note: "Use for labeled actions" },
|
|
31
31
|
],
|
|
32
32
|
}),
|
|
33
|
-
ButtonGroup:
|
|
33
|
+
ButtonGroup: makeFragment({
|
|
34
34
|
meta: { name: "ButtonGroup", description: "Groups buttons", category: "actions", status: "stable" },
|
|
35
35
|
relations: [
|
|
36
36
|
{ component: "Button", relationship: "child", note: "Contains buttons" },
|
|
37
37
|
],
|
|
38
38
|
}),
|
|
39
|
-
TextField:
|
|
39
|
+
TextField: makeFragment({
|
|
40
40
|
meta: { name: "TextField", description: "Text input", category: "forms", status: "stable" },
|
|
41
41
|
relations: [
|
|
42
42
|
{ component: "Form", relationship: "parent", note: "Should be inside a Form" },
|
|
@@ -47,22 +47,22 @@ const baseSegments: Record<string, CompiledSegment> = {
|
|
|
47
47
|
whenNot: ["Use TextArea for multiline input"],
|
|
48
48
|
},
|
|
49
49
|
}),
|
|
50
|
-
Form:
|
|
50
|
+
Form: makeFragment({
|
|
51
51
|
meta: { name: "Form", description: "Form container", category: "forms", status: "stable" },
|
|
52
52
|
}),
|
|
53
|
-
Label:
|
|
53
|
+
Label: makeFragment({
|
|
54
54
|
meta: { name: "Label", description: "Form label", category: "forms", status: "stable" },
|
|
55
55
|
}),
|
|
56
|
-
Alert:
|
|
56
|
+
Alert: makeFragment({
|
|
57
57
|
meta: { name: "Alert", description: "Feedback alert", category: "feedback", status: "stable" },
|
|
58
58
|
}),
|
|
59
|
-
Toast:
|
|
59
|
+
Toast: makeFragment({
|
|
60
60
|
meta: { name: "Toast", description: "Toast notification", category: "feedback", status: "experimental" },
|
|
61
61
|
}),
|
|
62
|
-
OldButton:
|
|
62
|
+
OldButton: makeFragment({
|
|
63
63
|
meta: { name: "OldButton", description: "Use Button instead", category: "actions", status: "deprecated" },
|
|
64
64
|
}),
|
|
65
|
-
Link:
|
|
65
|
+
Link: makeFragment({
|
|
66
66
|
meta: { name: "Link", description: "Navigation link", category: "navigation", status: "stable" },
|
|
67
67
|
usage: {
|
|
68
68
|
when: ["User needs to navigate"],
|
|
@@ -74,25 +74,25 @@ const baseSegments: Record<string, CompiledSegment> = {
|
|
|
74
74
|
describe("analyzeComposition", () => {
|
|
75
75
|
describe("name validation", () => {
|
|
76
76
|
it("should split components into found and unknown", () => {
|
|
77
|
-
const result = analyzeComposition(
|
|
77
|
+
const result = analyzeComposition(baseFragments, ["Button", "NonExistent", "TextField", "AlsoFake"]);
|
|
78
78
|
expect(result.components).toEqual(["Button", "TextField"]);
|
|
79
79
|
expect(result.unknown).toEqual(["NonExistent", "AlsoFake"]);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
it("should handle all valid names", () => {
|
|
83
|
-
const result = analyzeComposition(
|
|
83
|
+
const result = analyzeComposition(baseFragments, ["Button", "TextField"]);
|
|
84
84
|
expect(result.components).toEqual(["Button", "TextField"]);
|
|
85
85
|
expect(result.unknown).toEqual([]);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it("should handle all unknown names", () => {
|
|
89
|
-
const result = analyzeComposition(
|
|
89
|
+
const result = analyzeComposition(baseFragments, ["Foo", "Bar"]);
|
|
90
90
|
expect(result.components).toEqual([]);
|
|
91
91
|
expect(result.unknown).toEqual(["Foo", "Bar"]);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
it("should handle empty input", () => {
|
|
95
|
-
const result = analyzeComposition(
|
|
95
|
+
const result = analyzeComposition(baseFragments, []);
|
|
96
96
|
expect(result.components).toEqual([]);
|
|
97
97
|
expect(result.unknown).toEqual([]);
|
|
98
98
|
expect(result.warnings).toEqual([]);
|
|
@@ -103,7 +103,7 @@ describe("analyzeComposition", () => {
|
|
|
103
103
|
|
|
104
104
|
describe("relation checks", () => {
|
|
105
105
|
it("should warn about missing parent", () => {
|
|
106
|
-
const result = analyzeComposition(
|
|
106
|
+
const result = analyzeComposition(baseFragments, ["TextField"]);
|
|
107
107
|
const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
|
|
108
108
|
expect(parentWarning).toBeDefined();
|
|
109
109
|
expect(parentWarning!.component).toBe("TextField");
|
|
@@ -111,13 +111,13 @@ describe("analyzeComposition", () => {
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
it("should not warn about parent when parent is selected", () => {
|
|
114
|
-
const result = analyzeComposition(
|
|
114
|
+
const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
|
|
115
115
|
const parentWarning = result.warnings.find((w) => w.type === "missing_parent");
|
|
116
116
|
expect(parentWarning).toBeUndefined();
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it("should suggest missing children", () => {
|
|
120
|
-
const result = analyzeComposition(
|
|
120
|
+
const result = analyzeComposition(baseFragments, ["ButtonGroup"]);
|
|
121
121
|
const childSuggestion = result.suggestions.find(
|
|
122
122
|
(s) => s.component === "Button" && s.relationship === "child"
|
|
123
123
|
);
|
|
@@ -126,7 +126,7 @@ describe("analyzeComposition", () => {
|
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
it("should warn about missing composition peers", () => {
|
|
129
|
-
const result = analyzeComposition(
|
|
129
|
+
const result = analyzeComposition(baseFragments, ["Button"]);
|
|
130
130
|
const compWarning = result.warnings.find((w) => w.type === "missing_composition");
|
|
131
131
|
expect(compWarning).toBeDefined();
|
|
132
132
|
expect(compWarning!.component).toBe("Button");
|
|
@@ -134,13 +134,13 @@ describe("analyzeComposition", () => {
|
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
it("should not warn when composition peer is selected", () => {
|
|
137
|
-
const result = analyzeComposition(
|
|
137
|
+
const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup"]);
|
|
138
138
|
const compWarning = result.warnings.find((w) => w.type === "missing_composition");
|
|
139
139
|
expect(compWarning).toBeUndefined();
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it("should suggest missing siblings", () => {
|
|
143
|
-
const result = analyzeComposition(
|
|
143
|
+
const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
|
|
144
144
|
const siblingSuggestion = result.suggestions.find(
|
|
145
145
|
(s) => s.component === "Label" && s.relationship === "sibling"
|
|
146
146
|
);
|
|
@@ -149,7 +149,7 @@ describe("analyzeComposition", () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it("should warn about redundant alternatives", () => {
|
|
152
|
-
const result = analyzeComposition(
|
|
152
|
+
const result = analyzeComposition(baseFragments, ["Button", "IconButton"]);
|
|
153
153
|
const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
|
|
154
154
|
expect(altWarning).toBeDefined();
|
|
155
155
|
expect(altWarning!.component).toBe("Button");
|
|
@@ -157,7 +157,7 @@ describe("analyzeComposition", () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
it("should not warn when only one alternative is selected", () => {
|
|
160
|
-
const result = analyzeComposition(
|
|
160
|
+
const result = analyzeComposition(baseFragments, ["Button"]);
|
|
161
161
|
const altWarning = result.warnings.find((w) => w.type === "redundant_alternative");
|
|
162
162
|
expect(altWarning).toBeUndefined();
|
|
163
163
|
});
|
|
@@ -165,7 +165,7 @@ describe("analyzeComposition", () => {
|
|
|
165
165
|
|
|
166
166
|
describe("usage conflict checks", () => {
|
|
167
167
|
it("should emit guideline when whenNot mentions another selected component", () => {
|
|
168
|
-
const result = analyzeComposition(
|
|
168
|
+
const result = analyzeComposition(baseFragments, ["Button", "Link"]);
|
|
169
169
|
const conflict = result.guidelines.find(
|
|
170
170
|
(g) => g.component === "Link" && g.guideline.includes("Button")
|
|
171
171
|
);
|
|
@@ -173,14 +173,14 @@ describe("analyzeComposition", () => {
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
it("should not emit guideline when no conflict exists", () => {
|
|
176
|
-
const result = analyzeComposition(
|
|
176
|
+
const result = analyzeComposition(baseFragments, ["Button", "Alert"]);
|
|
177
177
|
expect(result.guidelines).toEqual([]);
|
|
178
178
|
});
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
describe("status warnings", () => {
|
|
182
182
|
it("should warn about deprecated components", () => {
|
|
183
|
-
const result = analyzeComposition(
|
|
183
|
+
const result = analyzeComposition(baseFragments, ["OldButton"]);
|
|
184
184
|
const depWarning = result.warnings.find((w) => w.type === "deprecated");
|
|
185
185
|
expect(depWarning).toBeDefined();
|
|
186
186
|
expect(depWarning!.component).toBe("OldButton");
|
|
@@ -188,7 +188,7 @@ describe("analyzeComposition", () => {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
it("should warn about experimental components", () => {
|
|
191
|
-
const result = analyzeComposition(
|
|
191
|
+
const result = analyzeComposition(baseFragments, ["Toast"]);
|
|
192
192
|
const expWarning = result.warnings.find((w) => w.type === "experimental");
|
|
193
193
|
expect(expWarning).toBeDefined();
|
|
194
194
|
expect(expWarning!.component).toBe("Toast");
|
|
@@ -196,7 +196,7 @@ describe("analyzeComposition", () => {
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
it("should not warn about stable components", () => {
|
|
199
|
-
const result = analyzeComposition(
|
|
199
|
+
const result = analyzeComposition(baseFragments, ["Button"]);
|
|
200
200
|
const statusWarnings = result.warnings.filter(
|
|
201
201
|
(w) => w.type === "deprecated" || w.type === "experimental"
|
|
202
202
|
);
|
|
@@ -206,7 +206,7 @@ describe("analyzeComposition", () => {
|
|
|
206
206
|
|
|
207
207
|
describe("category gap analysis", () => {
|
|
208
208
|
it("should suggest feedback component when forms are selected without feedback", () => {
|
|
209
|
-
const result = analyzeComposition(
|
|
209
|
+
const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
|
|
210
210
|
const gapSuggestion = result.suggestions.find(
|
|
211
211
|
(s) => s.relationship === "category_gap"
|
|
212
212
|
);
|
|
@@ -216,7 +216,7 @@ describe("analyzeComposition", () => {
|
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
it("should suggest feedback component when actions are selected without feedback", () => {
|
|
219
|
-
const result = analyzeComposition(
|
|
219
|
+
const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup"]);
|
|
220
220
|
const gapSuggestion = result.suggestions.find(
|
|
221
221
|
(s) => s.relationship === "category_gap"
|
|
222
222
|
);
|
|
@@ -225,7 +225,7 @@ describe("analyzeComposition", () => {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
it("should not suggest category gap when feedback is already present", () => {
|
|
228
|
-
const result = analyzeComposition(
|
|
228
|
+
const result = analyzeComposition(baseFragments, ["Button", "ButtonGroup", "Alert"]);
|
|
229
229
|
const gapSuggestion = result.suggestions.find(
|
|
230
230
|
(s) => s.relationship === "category_gap"
|
|
231
231
|
);
|
|
@@ -234,7 +234,7 @@ describe("analyzeComposition", () => {
|
|
|
234
234
|
|
|
235
235
|
it("should prefer stable components over experimental in category gap suggestions", () => {
|
|
236
236
|
// Alert is stable, Toast is experimental — should pick Alert
|
|
237
|
-
const result = analyzeComposition(
|
|
237
|
+
const result = analyzeComposition(baseFragments, ["TextField", "Form"]);
|
|
238
238
|
const gapSuggestion = result.suggestions.find(
|
|
239
239
|
(s) => s.relationship === "category_gap"
|
|
240
240
|
);
|
|
@@ -244,7 +244,7 @@ describe("analyzeComposition", () => {
|
|
|
244
244
|
|
|
245
245
|
describe("deduplication", () => {
|
|
246
246
|
it("should not suggest the same component twice", () => {
|
|
247
|
-
const result = analyzeComposition(
|
|
247
|
+
const result = analyzeComposition(baseFragments, ["ButtonGroup", "Button"]);
|
|
248
248
|
const buttonSuggestions = result.suggestions.filter(
|
|
249
249
|
(s) => s.component === "Button"
|
|
250
250
|
);
|
|
@@ -255,7 +255,7 @@ describe("analyzeComposition", () => {
|
|
|
255
255
|
|
|
256
256
|
describe("context parameter", () => {
|
|
257
257
|
it("should accept optional context without error", () => {
|
|
258
|
-
const result = analyzeComposition(
|
|
258
|
+
const result = analyzeComposition(baseFragments, ["Button"], "building a form page");
|
|
259
259
|
expect(result.components).toEqual(["Button"]);
|
|
260
260
|
});
|
|
261
261
|
});
|
package/src/core/composition.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CompiledFragment, RelationshipType } from "./types.js";
|
|
2
2
|
import type { ComponentGraph } from "@fragments-sdk/context/graph";
|
|
3
3
|
import { ComponentGraphEngine } from "@fragments-sdk/context/graph";
|
|
4
4
|
|
|
@@ -66,12 +66,12 @@ const CATEGORY_AFFINITIES: Record<string, string[]> = {
|
|
|
66
66
|
* Browser-safe: no Node.js APIs used.
|
|
67
67
|
*/
|
|
68
68
|
export function analyzeComposition(
|
|
69
|
-
|
|
69
|
+
fragments: Record<string, CompiledFragment>,
|
|
70
70
|
componentNames: string[],
|
|
71
71
|
_context?: string,
|
|
72
72
|
options?: { graph?: ComponentGraph },
|
|
73
73
|
): CompositionAnalysis {
|
|
74
|
-
const allNames = new Set(Object.keys(
|
|
74
|
+
const allNames = new Set(Object.keys(fragments));
|
|
75
75
|
|
|
76
76
|
// 1. Validate names
|
|
77
77
|
const components: string[] = [];
|
|
@@ -93,11 +93,11 @@ export function analyzeComposition(
|
|
|
93
93
|
const suggestedSet = new Set<string>();
|
|
94
94
|
|
|
95
95
|
for (const name of components) {
|
|
96
|
-
const
|
|
96
|
+
const fragment = fragments[name];
|
|
97
97
|
|
|
98
98
|
// 2. Relation checks
|
|
99
|
-
if (
|
|
100
|
-
for (const rel of
|
|
99
|
+
if (fragment.relations) {
|
|
100
|
+
for (const rel of fragment.relations) {
|
|
101
101
|
switch (rel.relationship) {
|
|
102
102
|
case "parent":
|
|
103
103
|
if (!selectedSet.has(rel.component)) {
|
|
@@ -160,8 +160,8 @@ export function analyzeComposition(
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// 3. Usage conflict checks (whenNot)
|
|
163
|
-
if (
|
|
164
|
-
for (const whenNotEntry of
|
|
163
|
+
if (fragment.usage?.whenNot) {
|
|
164
|
+
for (const whenNotEntry of fragment.usage.whenNot) {
|
|
165
165
|
const lower = whenNotEntry.toLowerCase();
|
|
166
166
|
for (const other of components) {
|
|
167
167
|
if (other !== name && lower.includes(other.toLowerCase())) {
|
|
@@ -175,15 +175,15 @@ export function analyzeComposition(
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// 4. Status warnings
|
|
178
|
-
if (
|
|
178
|
+
if (fragment.meta.status === "deprecated") {
|
|
179
179
|
warnings.push({
|
|
180
180
|
type: "deprecated",
|
|
181
181
|
component: name,
|
|
182
|
-
message:
|
|
183
|
-
? `"${name}" is deprecated: ${
|
|
182
|
+
message: fragment.meta.description
|
|
183
|
+
? `"${name}" is deprecated: ${fragment.meta.description}`
|
|
184
184
|
: `"${name}" is deprecated`,
|
|
185
185
|
});
|
|
186
|
-
} else if (
|
|
186
|
+
} else if (fragment.meta.status === "experimental") {
|
|
187
187
|
warnings.push({
|
|
188
188
|
type: "experimental",
|
|
189
189
|
component: name,
|
|
@@ -194,7 +194,7 @@ export function analyzeComposition(
|
|
|
194
194
|
|
|
195
195
|
// 5. Category gap analysis
|
|
196
196
|
const selectedCategories = new Set(
|
|
197
|
-
components.map((name) =>
|
|
197
|
+
components.map((name) => fragments[name].meta.category)
|
|
198
198
|
);
|
|
199
199
|
|
|
200
200
|
for (const [category, affinities] of Object.entries(CATEGORY_AFFINITIES)) {
|
|
@@ -205,7 +205,7 @@ export function analyzeComposition(
|
|
|
205
205
|
|
|
206
206
|
// Find the best component from the needed category
|
|
207
207
|
const candidate = findBestCategoryCandidate(
|
|
208
|
-
|
|
208
|
+
fragments,
|
|
209
209
|
neededCategory,
|
|
210
210
|
selectedSet,
|
|
211
211
|
suggestedSet
|
|
@@ -216,7 +216,7 @@ export function analyzeComposition(
|
|
|
216
216
|
reason: `Compositions using "${category}" components often benefit from a "${neededCategory}" component`,
|
|
217
217
|
relationship: "category_gap",
|
|
218
218
|
sourceComponent: components.find(
|
|
219
|
-
(n) =>
|
|
219
|
+
(n) => fragments[n].meta.category === category
|
|
220
220
|
)!,
|
|
221
221
|
});
|
|
222
222
|
suggestedSet.add(candidate);
|
|
@@ -289,7 +289,7 @@ export function analyzeComposition(
|
|
|
289
289
|
* Prefers stable components and avoids already-selected or already-suggested ones.
|
|
290
290
|
*/
|
|
291
291
|
function findBestCategoryCandidate(
|
|
292
|
-
|
|
292
|
+
fragments: Record<string, CompiledFragment>,
|
|
293
293
|
category: string,
|
|
294
294
|
selectedSet: Set<string>,
|
|
295
295
|
suggestedSet: Set<string>
|
|
@@ -297,11 +297,11 @@ function findBestCategoryCandidate(
|
|
|
297
297
|
let best: string | null = null;
|
|
298
298
|
let bestScore = -1;
|
|
299
299
|
|
|
300
|
-
for (const [name,
|
|
301
|
-
if (
|
|
300
|
+
for (const [name, fragment] of Object.entries(fragments)) {
|
|
301
|
+
if (fragment.meta.category !== category) continue;
|
|
302
302
|
if (selectedSet.has(name) || suggestedSet.has(name)) continue;
|
|
303
303
|
|
|
304
|
-
const status =
|
|
304
|
+
const status = fragment.meta.status ?? "stable";
|
|
305
305
|
let score = 0;
|
|
306
306
|
if (status === "stable") score = 3;
|
|
307
307
|
else if (status === "beta") score = 2;
|
package/src/core/config.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import { createJiti } from 'jiti';
|
|
4
4
|
import { BRAND } from './constants.js';
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
5
|
+
import type { FragmentsConfig } from './types.js';
|
|
6
|
+
import { fragmentsConfigSchema } from './schema.js';
|
|
7
7
|
|
|
8
|
-
const DEFAULT_CONFIG:
|
|
8
|
+
const DEFAULT_CONFIG: FragmentsConfig = {
|
|
9
9
|
include: [
|
|
10
|
-
`src/**/*${BRAND.fileExtension}`, // *.
|
|
10
|
+
`src/**/*${BRAND.fileExtension}`, // *.fragment.tsx files
|
|
11
11
|
'src/**/*.stories.tsx', // Storybook stories (auto-converted)
|
|
12
12
|
],
|
|
13
13
|
exclude: ['**/node_modules/**'],
|
|
@@ -43,7 +43,7 @@ export function findConfigFile(startDir: string = process.cwd()): string | null
|
|
|
43
43
|
* Load and validate the config file
|
|
44
44
|
*/
|
|
45
45
|
export async function loadConfig(configPath?: string): Promise<{
|
|
46
|
-
config:
|
|
46
|
+
config: FragmentsConfig;
|
|
47
47
|
configDir: string;
|
|
48
48
|
}> {
|
|
49
49
|
const resolvedPath = configPath ?? findConfigFile();
|
|
@@ -62,7 +62,7 @@ export async function loadConfig(configPath?: string): Promise<{
|
|
|
62
62
|
});
|
|
63
63
|
const rawConfig = await jiti.import(resolvedPath);
|
|
64
64
|
|
|
65
|
-
const result =
|
|
65
|
+
const result = fragmentsConfigSchema.safeParse(rawConfig);
|
|
66
66
|
|
|
67
67
|
if (!result.success) {
|
|
68
68
|
const errors = result.error.errors
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { FragmentDefinition, CompiledFragment, FragmentComponent, BlockDefinition, CompiledBlock } from './types.js';
|
|
2
|
+
import { fragmentDefinitionSchema, blockDefinitionSchema } from './schema.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Define a
|
|
5
|
+
* Define a fragment for a component.
|
|
6
6
|
*
|
|
7
|
-
* This is the main API for creating
|
|
7
|
+
* This is the main API for creating fragment documentation.
|
|
8
8
|
* It provides runtime validation and type safety.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
|
-
* import {
|
|
12
|
+
* import { defineFragment } from '@fragments/core';
|
|
13
13
|
* import { Button } from './Button';
|
|
14
14
|
*
|
|
15
|
-
* export default
|
|
15
|
+
* export default defineFragment({
|
|
16
16
|
* component: Button,
|
|
17
17
|
* meta: {
|
|
18
18
|
* name: 'Button',
|
|
@@ -41,18 +41,18 @@ import { segmentDefinitionSchema, blockDefinitionSchema } from './schema.js';
|
|
|
41
41
|
* });
|
|
42
42
|
* ```
|
|
43
43
|
*/
|
|
44
|
-
export function
|
|
45
|
-
definition:
|
|
46
|
-
):
|
|
44
|
+
export function defineFragment<TProps>(
|
|
45
|
+
definition: FragmentDefinition<TProps>
|
|
46
|
+
): FragmentDefinition<TProps> {
|
|
47
47
|
// Validate at runtime in development
|
|
48
48
|
if (process.env.NODE_ENV !== 'production') {
|
|
49
|
-
const result =
|
|
49
|
+
const result = fragmentDefinitionSchema.safeParse(definition);
|
|
50
50
|
if (!result.success) {
|
|
51
51
|
const errors = result.error.errors
|
|
52
52
|
.map((e) => ` - ${e.path.join('.')}: ${e.message}`)
|
|
53
53
|
.join('\n');
|
|
54
54
|
throw new Error(
|
|
55
|
-
`Invalid
|
|
55
|
+
`Invalid fragment definition for "${definition.meta?.name || 'unknown'}":\n${errors}`
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -61,19 +61,13 @@ export function defineSegment<TProps>(
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
* @see defineSegment
|
|
66
|
-
*/
|
|
67
|
-
export const defineFragment = defineSegment;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Compile a segment definition to JSON-serializable format.
|
|
64
|
+
* Compile a fragment definition to JSON-serializable format.
|
|
71
65
|
* Used for generating fragments.json for AI consumption.
|
|
72
66
|
*/
|
|
73
|
-
export function
|
|
74
|
-
definition:
|
|
67
|
+
export function compileFragment(
|
|
68
|
+
definition: FragmentDefinition,
|
|
75
69
|
filePath: string
|
|
76
|
-
):
|
|
70
|
+
): CompiledFragment {
|
|
77
71
|
return {
|
|
78
72
|
filePath,
|
|
79
73
|
meta: definition.meta,
|
|
@@ -144,4 +138,4 @@ export const compileRecipe = compileBlock;
|
|
|
144
138
|
/**
|
|
145
139
|
* Type helper for extracting props type from a component
|
|
146
140
|
*/
|
|
147
|
-
export type InferProps<T> = T extends
|
|
141
|
+
export type InferProps<T> = T extends FragmentComponent<infer P> ? P : never;
|
package/src/core/discovery.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { resolve, dirname, basename } from 'node:path';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import fg from 'fast-glob';
|
|
5
|
-
import type {
|
|
5
|
+
import type { FragmentsConfig } from './types.js';
|
|
6
6
|
import { BRAND } from './constants.js';
|
|
7
7
|
|
|
8
8
|
export interface DiscoveredFile {
|
|
@@ -56,10 +56,10 @@ export async function discoverBlockFiles(
|
|
|
56
56
|
export const discoverRecipeFiles = discoverBlockFiles;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Discover
|
|
59
|
+
* Discover fragment files matching the config patterns
|
|
60
60
|
*/
|
|
61
|
-
export async function
|
|
62
|
-
config:
|
|
61
|
+
export async function discoverFragmentFiles(
|
|
62
|
+
config: FragmentsConfig,
|
|
63
63
|
configDir: string
|
|
64
64
|
): Promise<DiscoveredFile[]> {
|
|
65
65
|
const files = await fg(config.include, {
|
|
@@ -78,7 +78,7 @@ export async function discoverSegmentFiles(
|
|
|
78
78
|
* Discover component files for coverage validation
|
|
79
79
|
*/
|
|
80
80
|
export async function discoverComponentFiles(
|
|
81
|
-
config:
|
|
81
|
+
config: FragmentsConfig,
|
|
82
82
|
configDir: string
|
|
83
83
|
): Promise<DiscoveredFile[]> {
|
|
84
84
|
if (!config.components || config.components.length === 0) {
|
|
@@ -89,7 +89,7 @@ export async function discoverComponentFiles(
|
|
|
89
89
|
cwd: configDir,
|
|
90
90
|
ignore: [
|
|
91
91
|
...(config.exclude ?? []),
|
|
92
|
-
// Exclude
|
|
92
|
+
// Exclude fragment files themselves
|
|
93
93
|
...config.include,
|
|
94
94
|
// Exclude test files
|
|
95
95
|
'**/*.test.*',
|
package/src/core/figma.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```tsx
|
|
9
|
-
* import {
|
|
9
|
+
* import { defineFragment, figma } from '@fragments/core';
|
|
10
10
|
*
|
|
11
|
-
* export default
|
|
11
|
+
* export default defineFragment({
|
|
12
12
|
* component: Button,
|
|
13
13
|
* meta: {
|
|
14
14
|
* name: 'Button',
|