@fragments-sdk/cli 0.2.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 +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
extractStyleLocations,
|
|
4
|
+
applyPatch,
|
|
5
|
+
applyPatches,
|
|
6
|
+
findStyleOccurrences,
|
|
7
|
+
type StyleLocation,
|
|
8
|
+
} from "../ast-utils.js";
|
|
9
|
+
|
|
10
|
+
describe("ast-utils", () => {
|
|
11
|
+
describe("extractStyleLocations", () => {
|
|
12
|
+
it("extracts inline style locations from JSX", () => {
|
|
13
|
+
const source = `
|
|
14
|
+
function Button() {
|
|
15
|
+
return <button style={{ backgroundColor: "#ff0000", padding: "16px" }}>Click</button>;
|
|
16
|
+
}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const locations = extractStyleLocations(source, "Button.tsx");
|
|
20
|
+
|
|
21
|
+
expect(locations).toHaveLength(2);
|
|
22
|
+
expect(locations.find(l => l.property === "backgroundColor")).toMatchObject({
|
|
23
|
+
type: "inline",
|
|
24
|
+
value: "#ff0000",
|
|
25
|
+
file: "Button.tsx",
|
|
26
|
+
});
|
|
27
|
+
expect(locations.find(l => l.property === "padding")).toMatchObject({
|
|
28
|
+
type: "inline",
|
|
29
|
+
value: "16px",
|
|
30
|
+
file: "Button.tsx",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("extracts emotion css prop locations", () => {
|
|
35
|
+
const source = `
|
|
36
|
+
function Button() {
|
|
37
|
+
return <button css={{ color: "blue", margin: "8px" }}>Click</button>;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const locations = extractStyleLocations(source, "Button.tsx");
|
|
42
|
+
|
|
43
|
+
expect(locations).toHaveLength(2);
|
|
44
|
+
expect(locations[0].type).toBe("emotion");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("extracts styled-components template literal styles", () => {
|
|
48
|
+
const source = `
|
|
49
|
+
const Button = styled.button\`
|
|
50
|
+
background-color: #ff0000;
|
|
51
|
+
padding: 16px;
|
|
52
|
+
margin: 8px;
|
|
53
|
+
\`;
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const locations = extractStyleLocations(source, "Button.tsx");
|
|
57
|
+
|
|
58
|
+
expect(locations.length).toBeGreaterThanOrEqual(2);
|
|
59
|
+
expect(locations[0].type).toBe("styled-components");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("extracts css`` template literal styles", () => {
|
|
63
|
+
const source = `
|
|
64
|
+
const buttonStyles = css\`
|
|
65
|
+
color: red;
|
|
66
|
+
font-size: 14px;
|
|
67
|
+
\`;
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const locations = extractStyleLocations(source, "styles.ts");
|
|
71
|
+
|
|
72
|
+
expect(locations.length).toBeGreaterThanOrEqual(1);
|
|
73
|
+
expect(locations[0].type).toBe("styled-components");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("handles numeric values", () => {
|
|
77
|
+
const source = `
|
|
78
|
+
function Box() {
|
|
79
|
+
return <div style={{ width: 100, height: 200 }}>Box</div>;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const locations = extractStyleLocations(source, "Box.tsx");
|
|
84
|
+
|
|
85
|
+
expect(locations).toHaveLength(2);
|
|
86
|
+
expect(locations.find(l => l.property === "width")?.value).toBe("100");
|
|
87
|
+
expect(locations.find(l => l.property === "height")?.value).toBe("200");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("handles template literal values in style objects", () => {
|
|
91
|
+
const source = `
|
|
92
|
+
function Box() {
|
|
93
|
+
return <div style={{ padding: \`16px\` }}>Box</div>;
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const locations = extractStyleLocations(source, "Box.tsx");
|
|
98
|
+
|
|
99
|
+
expect(locations).toHaveLength(1);
|
|
100
|
+
expect(locations[0].property).toBe("padding");
|
|
101
|
+
expect(locations[0].value).toBe("16px");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles string literal property keys", () => {
|
|
105
|
+
const source = `
|
|
106
|
+
function Box() {
|
|
107
|
+
return <div style={{ "background-color": "#fff" }}>Box</div>;
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const locations = extractStyleLocations(source, "Box.tsx");
|
|
112
|
+
|
|
113
|
+
expect(locations).toHaveLength(1);
|
|
114
|
+
expect(locations[0].property).toBe("background-color");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns empty array for source without styles", () => {
|
|
118
|
+
const source = `
|
|
119
|
+
function Button({ children }) {
|
|
120
|
+
return <button className="btn">{children}</button>;
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const locations = extractStyleLocations(source, "Button.tsx");
|
|
125
|
+
|
|
126
|
+
expect(locations).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("handles parse errors gracefully", () => {
|
|
130
|
+
const source = `this is not valid {{{`;
|
|
131
|
+
|
|
132
|
+
const locations = extractStyleLocations(source, "Invalid.tsx");
|
|
133
|
+
|
|
134
|
+
expect(locations).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("includes line and column information", () => {
|
|
138
|
+
const source = `function Button() {
|
|
139
|
+
return <button style={{ color: "red" }}>Click</button>;
|
|
140
|
+
}`;
|
|
141
|
+
|
|
142
|
+
const locations = extractStyleLocations(source, "Button.tsx");
|
|
143
|
+
|
|
144
|
+
expect(locations).toHaveLength(1);
|
|
145
|
+
expect(locations[0].line).toBeGreaterThan(0);
|
|
146
|
+
expect(typeof locations[0].column).toBe("number");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("applyPatch", () => {
|
|
151
|
+
it("replaces inline style value", () => {
|
|
152
|
+
const source = `function Button() {
|
|
153
|
+
return <button style={{ backgroundColor: "#ff0000" }}>Click</button>;
|
|
154
|
+
}`;
|
|
155
|
+
|
|
156
|
+
const location: StyleLocation = {
|
|
157
|
+
file: "Button.tsx",
|
|
158
|
+
line: 2,
|
|
159
|
+
column: 27,
|
|
160
|
+
type: "inline",
|
|
161
|
+
property: "backgroundColor",
|
|
162
|
+
value: "#ff0000",
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = applyPatch(source, location, "#ff0000", "var(--color-primary)");
|
|
166
|
+
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
expect(result.code).toContain("var(--color-primary)");
|
|
169
|
+
expect(result.code).not.toContain("#ff0000");
|
|
170
|
+
expect(result.diff).toBeDefined();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns error when old value not found", () => {
|
|
174
|
+
const source = `function Button() {
|
|
175
|
+
return <button style={{ backgroundColor: "#0000ff" }}>Click</button>;
|
|
176
|
+
}`;
|
|
177
|
+
|
|
178
|
+
const location: StyleLocation = {
|
|
179
|
+
file: "Button.tsx",
|
|
180
|
+
line: 2,
|
|
181
|
+
column: 27,
|
|
182
|
+
type: "inline",
|
|
183
|
+
property: "backgroundColor",
|
|
184
|
+
value: "#ff0000",
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const result = applyPatch(source, location, "#ff0000", "var(--color-primary)");
|
|
188
|
+
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
expect(result.error).toBeDefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("generates unified diff", () => {
|
|
194
|
+
const source = `function Button() {
|
|
195
|
+
return <button style={{ color: "red" }}>Click</button>;
|
|
196
|
+
}`;
|
|
197
|
+
|
|
198
|
+
const location: StyleLocation = {
|
|
199
|
+
file: "Button.tsx",
|
|
200
|
+
line: 2,
|
|
201
|
+
column: 27,
|
|
202
|
+
type: "inline",
|
|
203
|
+
property: "color",
|
|
204
|
+
value: "red",
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = applyPatch(source, location, "red", "var(--color-error)");
|
|
208
|
+
|
|
209
|
+
expect(result.success).toBe(true);
|
|
210
|
+
expect(result.diff).toContain("--- a/Button.tsx");
|
|
211
|
+
expect(result.diff).toContain("+++ b/Button.tsx");
|
|
212
|
+
expect(result.diff).toContain("-");
|
|
213
|
+
expect(result.diff).toContain("+");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("handles parse errors gracefully", () => {
|
|
217
|
+
const source = `this is not valid {{{`;
|
|
218
|
+
|
|
219
|
+
const location: StyleLocation = {
|
|
220
|
+
file: "Invalid.tsx",
|
|
221
|
+
line: 1,
|
|
222
|
+
column: 0,
|
|
223
|
+
type: "inline",
|
|
224
|
+
property: "color",
|
|
225
|
+
value: "red",
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = applyPatch(source, location, "red", "blue");
|
|
229
|
+
|
|
230
|
+
expect(result.success).toBe(false);
|
|
231
|
+
expect(result.error).toContain("parse");
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("applyPatches", () => {
|
|
236
|
+
it("applies multiple patches in correct order", () => {
|
|
237
|
+
const source = `function Component() {
|
|
238
|
+
return (
|
|
239
|
+
<div style={{ backgroundColor: "#ff0000" }}>
|
|
240
|
+
<span style={{ color: "#0000ff" }}>Text</span>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}`;
|
|
244
|
+
|
|
245
|
+
const patches = [
|
|
246
|
+
{
|
|
247
|
+
location: {
|
|
248
|
+
file: "Component.tsx",
|
|
249
|
+
line: 3,
|
|
250
|
+
column: 18,
|
|
251
|
+
type: "inline" as const,
|
|
252
|
+
property: "backgroundColor",
|
|
253
|
+
value: "#ff0000",
|
|
254
|
+
},
|
|
255
|
+
oldValue: "#ff0000",
|
|
256
|
+
newValue: "var(--bg-primary)",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
location: {
|
|
260
|
+
file: "Component.tsx",
|
|
261
|
+
line: 4,
|
|
262
|
+
column: 20,
|
|
263
|
+
type: "inline" as const,
|
|
264
|
+
property: "color",
|
|
265
|
+
value: "#0000ff",
|
|
266
|
+
},
|
|
267
|
+
oldValue: "#0000ff",
|
|
268
|
+
newValue: "var(--text-primary)",
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const result = applyPatches(source, patches);
|
|
273
|
+
|
|
274
|
+
expect(result.success).toBe(true);
|
|
275
|
+
expect(result.code).toContain("var(--bg-primary)");
|
|
276
|
+
expect(result.code).toContain("var(--text-primary)");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("returns error if any patch fails", () => {
|
|
280
|
+
const source = `function Component() {
|
|
281
|
+
return <div style={{ color: "red" }}>Text</div>;
|
|
282
|
+
}`;
|
|
283
|
+
|
|
284
|
+
const patches = [
|
|
285
|
+
{
|
|
286
|
+
location: {
|
|
287
|
+
file: "Component.tsx",
|
|
288
|
+
line: 2,
|
|
289
|
+
column: 24,
|
|
290
|
+
type: "inline" as const,
|
|
291
|
+
property: "color",
|
|
292
|
+
value: "red",
|
|
293
|
+
},
|
|
294
|
+
oldValue: "red",
|
|
295
|
+
newValue: "var(--color)",
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
location: {
|
|
299
|
+
file: "Component.tsx",
|
|
300
|
+
line: 2,
|
|
301
|
+
column: 24,
|
|
302
|
+
type: "inline" as const,
|
|
303
|
+
property: "backgroundColor",
|
|
304
|
+
value: "blue", // This value doesn't exist
|
|
305
|
+
},
|
|
306
|
+
oldValue: "blue",
|
|
307
|
+
newValue: "var(--bg)",
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
const result = applyPatches(source, patches);
|
|
312
|
+
|
|
313
|
+
expect(result.success).toBe(false);
|
|
314
|
+
expect(result.error).toBeDefined();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("combines diffs from all patches", () => {
|
|
318
|
+
const source = `function Box() {
|
|
319
|
+
return <div style={{ margin: "8px", padding: "16px" }}>Box</div>;
|
|
320
|
+
}`;
|
|
321
|
+
|
|
322
|
+
// Need actual locations from the source
|
|
323
|
+
const locations = extractStyleLocations(source, "Box.tsx");
|
|
324
|
+
const marginLoc = locations.find(l => l.property === "margin");
|
|
325
|
+
const paddingLoc = locations.find(l => l.property === "padding");
|
|
326
|
+
|
|
327
|
+
if (!marginLoc || !paddingLoc) {
|
|
328
|
+
throw new Error("Could not find style locations");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const patches = [
|
|
332
|
+
{
|
|
333
|
+
location: marginLoc,
|
|
334
|
+
oldValue: "8px",
|
|
335
|
+
newValue: "var(--spacing-sm)",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
location: paddingLoc,
|
|
339
|
+
oldValue: "16px",
|
|
340
|
+
newValue: "var(--spacing-md)",
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const result = applyPatches(source, patches);
|
|
345
|
+
|
|
346
|
+
expect(result.success).toBe(true);
|
|
347
|
+
expect(result.diff).toBeDefined();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("findStyleOccurrences", () => {
|
|
352
|
+
it("finds styles matching property and value patterns", () => {
|
|
353
|
+
const source = `
|
|
354
|
+
function Component() {
|
|
355
|
+
return (
|
|
356
|
+
<div style={{ backgroundColor: "#ff0000", color: "#ff0000" }}>
|
|
357
|
+
<span style={{ backgroundColor: "#0000ff" }}>Text</span>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
const occurrences = findStyleOccurrences(
|
|
364
|
+
source,
|
|
365
|
+
"Component.tsx",
|
|
366
|
+
"backgroundColor",
|
|
367
|
+
"#ff0000"
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(occurrences).toHaveLength(1);
|
|
371
|
+
expect(occurrences[0].property).toBe("backgroundColor");
|
|
372
|
+
expect(occurrences[0].value).toBe("#ff0000");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("supports regex patterns for property matching", () => {
|
|
376
|
+
const source = `
|
|
377
|
+
function Component() {
|
|
378
|
+
return <div style={{ backgroundColor: "red", borderColor: "red" }}>Box</div>;
|
|
379
|
+
}
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
const occurrences = findStyleOccurrences(
|
|
383
|
+
source,
|
|
384
|
+
"Component.tsx",
|
|
385
|
+
/.*Color$/,
|
|
386
|
+
"red"
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(occurrences).toHaveLength(2);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("supports regex patterns for value matching", () => {
|
|
393
|
+
const source = `
|
|
394
|
+
function Component() {
|
|
395
|
+
return <div style={{ padding: "16px", margin: "16rem" }}>Box</div>;
|
|
396
|
+
}
|
|
397
|
+
`;
|
|
398
|
+
|
|
399
|
+
const occurrences = findStyleOccurrences(
|
|
400
|
+
source,
|
|
401
|
+
"Component.tsx",
|
|
402
|
+
/.*/,
|
|
403
|
+
/16.*/
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(occurrences).toHaveLength(2);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("returns empty array when no matches", () => {
|
|
410
|
+
const source = `
|
|
411
|
+
function Component() {
|
|
412
|
+
return <div style={{ color: "blue" }}>Box</div>;
|
|
413
|
+
}
|
|
414
|
+
`;
|
|
415
|
+
|
|
416
|
+
const occurrences = findStyleOccurrences(
|
|
417
|
+
source,
|
|
418
|
+
"Component.tsx",
|
|
419
|
+
"backgroundColor",
|
|
420
|
+
"red"
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(occurrences).toHaveLength(0);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the enhancement scanner module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
6
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import {
|
|
10
|
+
scanFileForImports,
|
|
11
|
+
scanFile,
|
|
12
|
+
} from "../enhance/scanner.js";
|
|
13
|
+
|
|
14
|
+
let tempDir: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
tempDir = await mkdtemp(join(tmpdir(), "enhance-scanner-test-"));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function writeTestFile(name: string, content: string): Promise<string> {
|
|
25
|
+
const filePath = join(tempDir, name);
|
|
26
|
+
await writeFile(filePath, content, "utf-8");
|
|
27
|
+
return filePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("scanFileForImports", () => {
|
|
31
|
+
it("should extract named imports", async () => {
|
|
32
|
+
const filePath = await writeTestFile(
|
|
33
|
+
"named-imports.tsx",
|
|
34
|
+
`import { Button, Card } from '@acme/ui';`
|
|
35
|
+
);
|
|
36
|
+
const imports = await scanFileForImports(filePath);
|
|
37
|
+
|
|
38
|
+
expect(imports).toHaveLength(2);
|
|
39
|
+
expect(imports[0].componentName).toBe("Button");
|
|
40
|
+
expect(imports[0].source).toBe("@acme/ui");
|
|
41
|
+
expect(imports[0].isDefault).toBe(false);
|
|
42
|
+
expect(imports[1].componentName).toBe("Card");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should extract default imports", async () => {
|
|
46
|
+
const filePath = await writeTestFile(
|
|
47
|
+
"default-import.tsx",
|
|
48
|
+
`import Button from './Button';`
|
|
49
|
+
);
|
|
50
|
+
const imports = await scanFileForImports(filePath);
|
|
51
|
+
|
|
52
|
+
expect(imports).toHaveLength(1);
|
|
53
|
+
expect(imports[0].componentName).toBe("Button");
|
|
54
|
+
expect(imports[0].isDefault).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should handle aliased imports", async () => {
|
|
58
|
+
const filePath = await writeTestFile(
|
|
59
|
+
"aliased-import.tsx",
|
|
60
|
+
`import { Button as Btn } from '@acme/ui';`
|
|
61
|
+
);
|
|
62
|
+
const imports = await scanFileForImports(filePath);
|
|
63
|
+
|
|
64
|
+
expect(imports).toHaveLength(1);
|
|
65
|
+
expect(imports[0].componentName).toBe("Button");
|
|
66
|
+
expect(imports[0].localName).toBe("Btn");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should skip non-component imports (lowercase)", async () => {
|
|
70
|
+
const filePath = await writeTestFile(
|
|
71
|
+
"mixed-imports.tsx",
|
|
72
|
+
`import { useState, useEffect } from 'react';
|
|
73
|
+
import { Button } from '@acme/ui';`
|
|
74
|
+
);
|
|
75
|
+
const imports = await scanFileForImports(filePath);
|
|
76
|
+
|
|
77
|
+
expect(imports).toHaveLength(1);
|
|
78
|
+
expect(imports[0].componentName).toBe("Button");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("scanFile", () => {
|
|
83
|
+
it("should extract basic JSX usage", async () => {
|
|
84
|
+
const filePath = await writeTestFile(
|
|
85
|
+
"basic-usage.tsx",
|
|
86
|
+
`import { Button } from '@acme/ui';
|
|
87
|
+
|
|
88
|
+
export function App() {
|
|
89
|
+
return <Button variant="primary">Click me</Button>;
|
|
90
|
+
}`
|
|
91
|
+
);
|
|
92
|
+
const { imports, usages } = await scanFile(filePath);
|
|
93
|
+
|
|
94
|
+
expect(imports).toHaveLength(1);
|
|
95
|
+
expect(usages).toHaveLength(1);
|
|
96
|
+
expect(usages[0].componentName).toBe("Button");
|
|
97
|
+
expect(usages[0].props.static).toEqual({ variant: "primary" });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should extract multiple prop types", async () => {
|
|
101
|
+
const filePath = await writeTestFile(
|
|
102
|
+
"multiple-props.tsx",
|
|
103
|
+
`import { Button } from '@acme/ui';
|
|
104
|
+
|
|
105
|
+
export function App() {
|
|
106
|
+
return (
|
|
107
|
+
<Button
|
|
108
|
+
variant="primary"
|
|
109
|
+
disabled={true}
|
|
110
|
+
size={3}
|
|
111
|
+
onClick={handleClick}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}`
|
|
115
|
+
);
|
|
116
|
+
const { usages } = await scanFile(filePath);
|
|
117
|
+
|
|
118
|
+
expect(usages).toHaveLength(1);
|
|
119
|
+
expect(usages[0].props.static).toEqual({
|
|
120
|
+
variant: "primary",
|
|
121
|
+
disabled: true,
|
|
122
|
+
size: 3,
|
|
123
|
+
});
|
|
124
|
+
expect(usages[0].props.dynamic).toContain("onClick");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should track spread props", async () => {
|
|
128
|
+
const filePath = await writeTestFile(
|
|
129
|
+
"spread-props.tsx",
|
|
130
|
+
`import { Button } from '@acme/ui';
|
|
131
|
+
|
|
132
|
+
export function App() {
|
|
133
|
+
const buttonProps = { onClick: () => {} };
|
|
134
|
+
return <Button {...buttonProps} variant="primary" />;
|
|
135
|
+
}`
|
|
136
|
+
);
|
|
137
|
+
const { usages } = await scanFile(filePath);
|
|
138
|
+
|
|
139
|
+
expect(usages).toHaveLength(1);
|
|
140
|
+
expect(usages[0].hasSpreadProps).toBe(true);
|
|
141
|
+
expect(usages[0].props.spreads).toContain("buttonProps");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should detect conditional rendering", async () => {
|
|
145
|
+
const filePath = await writeTestFile(
|
|
146
|
+
"conditional.tsx",
|
|
147
|
+
`import { Button } from '@acme/ui';
|
|
148
|
+
|
|
149
|
+
export function App() {
|
|
150
|
+
const isVisible = true;
|
|
151
|
+
return <div>{isVisible && <Button />}</div>;
|
|
152
|
+
}`
|
|
153
|
+
);
|
|
154
|
+
const { usages } = await scanFile(filePath);
|
|
155
|
+
|
|
156
|
+
expect(usages).toHaveLength(1);
|
|
157
|
+
expect(usages[0].isConditional).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should extract parent element", async () => {
|
|
161
|
+
const filePath = await writeTestFile(
|
|
162
|
+
"nested.tsx",
|
|
163
|
+
`import { Button, Card } from '@acme/ui';
|
|
164
|
+
|
|
165
|
+
export function App() {
|
|
166
|
+
return (
|
|
167
|
+
<Card>
|
|
168
|
+
<Button />
|
|
169
|
+
</Card>
|
|
170
|
+
);
|
|
171
|
+
}`
|
|
172
|
+
);
|
|
173
|
+
const { usages } = await scanFile(filePath);
|
|
174
|
+
const buttonUsage = usages.find((u) => u.componentName === "Button");
|
|
175
|
+
|
|
176
|
+
expect(buttonUsage?.parentElement).toBe("Card");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should track specific components when provided", async () => {
|
|
180
|
+
const filePath = await writeTestFile(
|
|
181
|
+
"specific-components.tsx",
|
|
182
|
+
`import { Button, Card, Input } from '@acme/ui';
|
|
183
|
+
|
|
184
|
+
export function App() {
|
|
185
|
+
return (
|
|
186
|
+
<div>
|
|
187
|
+
<Button variant="primary" />
|
|
188
|
+
<Card title="Hello" />
|
|
189
|
+
<Input type="text" />
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}`
|
|
193
|
+
);
|
|
194
|
+
const tracked = new Set(["Button", "Card"]);
|
|
195
|
+
const { usages } = await scanFile(filePath, tracked);
|
|
196
|
+
|
|
197
|
+
expect(usages).toHaveLength(2);
|
|
198
|
+
expect(usages.map((u) => u.componentName).sort()).toEqual(["Button", "Card"]);
|
|
199
|
+
});
|
|
200
|
+
});
|