@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,409 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdir, rm, readdir, readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ MetricsStore,
7
+ createMetricsStore,
8
+ type ComplianceSnapshot,
9
+ } from "../metrics-store.js";
10
+
11
+ describe("metrics-store", () => {
12
+ let testDir: string;
13
+ let store: MetricsStore;
14
+
15
+ beforeEach(async () => {
16
+ // Create a unique temp directory for each test
17
+ testDir = join(tmpdir(), `metrics-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
18
+ await mkdir(testDir, { recursive: true });
19
+ store = createMetricsStore(testDir);
20
+ });
21
+
22
+ afterEach(async () => {
23
+ // Clean up temp directory
24
+ try {
25
+ await rm(testDir, { recursive: true, force: true });
26
+ } catch {
27
+ // Ignore cleanup errors
28
+ }
29
+ });
30
+
31
+ describe("saveSnapshot", () => {
32
+ it("saves a compliance snapshot to disk", async () => {
33
+ const snapshot: ComplianceSnapshot = {
34
+ timestamp: new Date().toISOString(),
35
+ component: "Button",
36
+ compliance: 85.5,
37
+ violations: 3,
38
+ };
39
+
40
+ const filepath = await store.saveSnapshot(snapshot);
41
+
42
+ expect(filepath).toContain("Button.json");
43
+
44
+ // Verify file exists by reading it
45
+ const content = await readFile(filepath, "utf-8");
46
+ const snapshots = JSON.parse(content);
47
+ expect(snapshots.length).toBe(1);
48
+ expect(snapshots[0].component).toBe("Button");
49
+ });
50
+
51
+ it("appends snapshots to existing file for same day/component", async () => {
52
+ const timestamp = new Date().toISOString();
53
+
54
+ const snapshot1: ComplianceSnapshot = {
55
+ timestamp,
56
+ component: "Button",
57
+ compliance: 80,
58
+ violations: 5,
59
+ };
60
+
61
+ const snapshot2: ComplianceSnapshot = {
62
+ timestamp,
63
+ component: "Button",
64
+ compliance: 85,
65
+ violations: 3,
66
+ };
67
+
68
+ const filepath1 = await store.saveSnapshot(snapshot1);
69
+ const filepath2 = await store.saveSnapshot(snapshot2);
70
+
71
+ // Both should write to same file
72
+ expect(filepath1).toBe(filepath2);
73
+
74
+ // File should contain both snapshots
75
+ const content = await readFile(filepath2, "utf-8");
76
+ const snapshots = JSON.parse(content);
77
+ expect(snapshots).toHaveLength(2);
78
+ });
79
+
80
+ it("includes violation details when provided", async () => {
81
+ const snapshot: ComplianceSnapshot = {
82
+ timestamp: new Date().toISOString(),
83
+ component: "Card",
84
+ compliance: 75,
85
+ violations: 2,
86
+ violationDetails: [
87
+ { property: "backgroundColor", issue: "Hardcoded value", severity: "error" },
88
+ { property: "padding", issue: "Non-token value", severity: "warning" },
89
+ ],
90
+ };
91
+
92
+ const filepath = await store.saveSnapshot(snapshot);
93
+ const content = await readFile(filepath, "utf-8");
94
+ const snapshots = JSON.parse(content);
95
+
96
+ expect(snapshots[0].violationDetails).toHaveLength(2);
97
+ });
98
+
99
+ it("includes git metadata when provided", async () => {
100
+ const snapshot: ComplianceSnapshot = {
101
+ timestamp: new Date().toISOString(),
102
+ component: "Button",
103
+ compliance: 90,
104
+ violations: 1,
105
+ commitHash: "abc123",
106
+ branch: "feature/new-button",
107
+ };
108
+
109
+ const filepath = await store.saveSnapshot(snapshot);
110
+ const content = await readFile(filepath, "utf-8");
111
+ const snapshots = JSON.parse(content);
112
+
113
+ expect(snapshots[0].commitHash).toBe("abc123");
114
+ expect(snapshots[0].branch).toBe("feature/new-button");
115
+ });
116
+ });
117
+
118
+ describe("getHistory", () => {
119
+ it("returns empty array when no metrics exist", async () => {
120
+ const history = await store.getHistory("Button");
121
+ expect(history).toHaveLength(0);
122
+ });
123
+
124
+ it("returns snapshots for a specific component", async () => {
125
+ const now = new Date();
126
+
127
+ await store.saveSnapshot({
128
+ timestamp: now.toISOString(),
129
+ component: "Button",
130
+ compliance: 85,
131
+ violations: 2,
132
+ });
133
+
134
+ await store.saveSnapshot({
135
+ timestamp: now.toISOString(),
136
+ component: "Card",
137
+ compliance: 90,
138
+ violations: 1,
139
+ });
140
+
141
+ const history = await store.getHistory("Button");
142
+
143
+ expect(history).toHaveLength(1);
144
+ expect(history[0].component).toBe("Button");
145
+ });
146
+
147
+ it("filters by date range", async () => {
148
+ const now = new Date();
149
+ const thirtyDaysAgo = new Date(now);
150
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 35);
151
+
152
+ // Save a recent snapshot
153
+ await store.saveSnapshot({
154
+ timestamp: now.toISOString(),
155
+ component: "Button",
156
+ compliance: 85,
157
+ violations: 2,
158
+ });
159
+
160
+ // Default is 30 days, so 35 days ago would be filtered out
161
+ const history = await store.getHistory("Button", { days: 30 });
162
+
163
+ expect(history).toHaveLength(1);
164
+ });
165
+
166
+ it("returns all components when component is 'all'", async () => {
167
+ const now = new Date();
168
+
169
+ await store.saveSnapshot({
170
+ timestamp: now.toISOString(),
171
+ component: "Button",
172
+ compliance: 85,
173
+ violations: 2,
174
+ });
175
+
176
+ await store.saveSnapshot({
177
+ timestamp: now.toISOString(),
178
+ component: "Card",
179
+ compliance: 90,
180
+ violations: 1,
181
+ });
182
+
183
+ const history = await store.getHistory("all");
184
+
185
+ expect(history).toHaveLength(2);
186
+ });
187
+
188
+ it("sorts snapshots by timestamp", async () => {
189
+ const now = new Date();
190
+ const earlier = new Date(now);
191
+ earlier.setHours(earlier.getHours() - 1);
192
+
193
+ await store.saveSnapshot({
194
+ timestamp: now.toISOString(),
195
+ component: "Button",
196
+ compliance: 90,
197
+ violations: 1,
198
+ });
199
+
200
+ await store.saveSnapshot({
201
+ timestamp: earlier.toISOString(),
202
+ component: "Button",
203
+ compliance: 80,
204
+ violations: 3,
205
+ });
206
+
207
+ const history = await store.getHistory("Button");
208
+
209
+ expect(history).toHaveLength(2);
210
+ expect(history[0].compliance).toBe(80); // Earlier first
211
+ expect(history[1].compliance).toBe(90); // Later second
212
+ });
213
+ });
214
+
215
+ describe("getSystemHistory", () => {
216
+ it("returns all component snapshots", async () => {
217
+ const now = new Date();
218
+
219
+ await store.saveSnapshot({
220
+ timestamp: now.toISOString(),
221
+ component: "Button",
222
+ compliance: 85,
223
+ violations: 2,
224
+ });
225
+
226
+ await store.saveSnapshot({
227
+ timestamp: now.toISOString(),
228
+ component: "Card",
229
+ compliance: 90,
230
+ violations: 1,
231
+ });
232
+
233
+ const history = await store.getSystemHistory();
234
+
235
+ expect(history).toHaveLength(2);
236
+ });
237
+ });
238
+
239
+ describe("getTrend", () => {
240
+ it("returns trend data grouped by day", async () => {
241
+ const now = new Date();
242
+
243
+ await store.saveSnapshot({
244
+ timestamp: now.toISOString(),
245
+ component: "Button",
246
+ compliance: 85,
247
+ violations: 2,
248
+ });
249
+
250
+ const trend = await store.getTrend("Button", { groupBy: "day" });
251
+
252
+ expect(trend.component).toBe("Button");
253
+ expect(trend.period).toBe("day");
254
+ expect(trend.dataPoints.length).toBeGreaterThanOrEqual(1);
255
+ });
256
+
257
+ it("calculates improving trend", async () => {
258
+ const now = new Date();
259
+ const yesterday = new Date(now);
260
+ yesterday.setDate(yesterday.getDate() - 1);
261
+
262
+ // Earlier: lower compliance
263
+ await store.saveSnapshot({
264
+ timestamp: yesterday.toISOString(),
265
+ component: "Button",
266
+ compliance: 70,
267
+ violations: 5,
268
+ });
269
+
270
+ // Now: higher compliance
271
+ await store.saveSnapshot({
272
+ timestamp: now.toISOString(),
273
+ component: "Button",
274
+ compliance: 95,
275
+ violations: 1,
276
+ });
277
+
278
+ const trend = await store.getTrend("Button");
279
+
280
+ // With significant improvement, trend should be improving
281
+ expect(trend.trend).toBe("improving");
282
+ });
283
+
284
+ it("calculates declining trend", async () => {
285
+ const now = new Date();
286
+ const yesterday = new Date(now);
287
+ yesterday.setDate(yesterday.getDate() - 1);
288
+
289
+ // Earlier: higher compliance
290
+ await store.saveSnapshot({
291
+ timestamp: yesterday.toISOString(),
292
+ component: "Button",
293
+ compliance: 95,
294
+ violations: 1,
295
+ });
296
+
297
+ // Now: lower compliance
298
+ await store.saveSnapshot({
299
+ timestamp: now.toISOString(),
300
+ component: "Button",
301
+ compliance: 70,
302
+ violations: 5,
303
+ });
304
+
305
+ const trend = await store.getTrend("Button");
306
+
307
+ expect(trend.trend).toBe("declining");
308
+ });
309
+
310
+ it("calculates stable trend for small changes", async () => {
311
+ const now = new Date();
312
+
313
+ await store.saveSnapshot({
314
+ timestamp: now.toISOString(),
315
+ component: "Button",
316
+ compliance: 85,
317
+ violations: 2,
318
+ });
319
+
320
+ const trend = await store.getTrend("Button");
321
+
322
+ // With only one data point, should be stable
323
+ expect(trend.trend).toBe("stable");
324
+ });
325
+
326
+ it("calculates average compliance", async () => {
327
+ const now = new Date();
328
+
329
+ await store.saveSnapshot({
330
+ timestamp: now.toISOString(),
331
+ component: "Button",
332
+ compliance: 80,
333
+ violations: 3,
334
+ });
335
+
336
+ await store.saveSnapshot({
337
+ timestamp: now.toISOString(),
338
+ component: "Button",
339
+ compliance: 90,
340
+ violations: 1,
341
+ });
342
+
343
+ const trend = await store.getTrend("Button");
344
+
345
+ expect(trend.averageCompliance).toBe(85);
346
+ });
347
+ });
348
+
349
+ describe("generateSparkline", () => {
350
+ it("generates ASCII sparkline from data points", () => {
351
+ const dataPoints = [
352
+ { compliance: 60 },
353
+ { compliance: 70 },
354
+ { compliance: 80 },
355
+ { compliance: 90 },
356
+ { compliance: 100 },
357
+ ];
358
+
359
+ const sparkline = store.generateSparkline(dataPoints);
360
+
361
+ expect(sparkline.length).toBe(5);
362
+ expect(sparkline).toMatch(/[▁▂▃▄▅▆▇█ ]+/);
363
+ });
364
+
365
+ it("handles empty data points", () => {
366
+ const sparkline = store.generateSparkline([]);
367
+ expect(sparkline).toBe("─");
368
+ });
369
+
370
+ it("handles single data point", () => {
371
+ const sparkline = store.generateSparkline([{ compliance: 80 }]);
372
+ expect(sparkline.length).toBe(1);
373
+ });
374
+
375
+ it("handles flat data (all same values)", () => {
376
+ const dataPoints = [
377
+ { compliance: 80 },
378
+ { compliance: 80 },
379
+ { compliance: 80 },
380
+ ];
381
+
382
+ const sparkline = store.generateSparkline(dataPoints);
383
+
384
+ // All same value, should be middle character
385
+ expect(sparkline.length).toBe(3);
386
+ });
387
+ });
388
+
389
+ describe("cleanup", () => {
390
+ it("deletes files older than retention period", async () => {
391
+ // This test is tricky because we can't easily create old files
392
+ // Just verify the method runs without error
393
+ const deleted = await store.cleanup(90);
394
+ expect(typeof deleted).toBe("number");
395
+ });
396
+
397
+ it("returns count of deleted files", async () => {
398
+ const deleted = await store.cleanup(90);
399
+ expect(deleted).toBe(0); // No files to delete in fresh test dir
400
+ });
401
+ });
402
+
403
+ describe("createMetricsStore", () => {
404
+ it("creates a MetricsStore instance", () => {
405
+ const store = createMetricsStore("/some/path");
406
+ expect(store).toBeInstanceOf(MetricsStore);
407
+ });
408
+ });
409
+ });
@@ -0,0 +1,186 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ generateTokenPatches,
7
+ generateCSSInJSPatches,
8
+ type Patch,
9
+ } from "../patch-generator.js";
10
+ import {
11
+ TokenRegistryManager,
12
+ createTokenRegistry,
13
+ } from "../token-registry.js";
14
+ import type { TokenConfig } from "../../core/index.js";
15
+
16
+ describe("patch-generator", () => {
17
+ let testDir: string;
18
+ let registry: TokenRegistryManager;
19
+
20
+ beforeEach(async () => {
21
+ testDir = join(tmpdir(), `patch-gen-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
22
+ await mkdir(testDir, { recursive: true });
23
+ registry = createTokenRegistry();
24
+
25
+ // Create token files for testing
26
+ const cssTokens = `
27
+ :root {
28
+ --color-primary: #ff0000;
29
+ --color-secondary: #0000ff;
30
+ --spacing-sm: 8px;
31
+ --spacing-md: 16px;
32
+ --spacing-lg: 24px;
33
+ }
34
+ `;
35
+ await writeFile(join(testDir, "tokens.css"), cssTokens);
36
+
37
+ const config: TokenConfig = {
38
+ include: ["tokens.css"],
39
+ };
40
+
41
+ await registry.initialize(config, testDir);
42
+ });
43
+
44
+ afterEach(async () => {
45
+ registry.clear();
46
+ try {
47
+ await rm(testDir, { recursive: true, force: true });
48
+ } catch {
49
+ // Ignore cleanup errors
50
+ }
51
+ });
52
+
53
+ describe("generateTokenPatches", () => {
54
+ it("generates patches for hardcoded color values", () => {
55
+ const styleDiffs = [
56
+ { property: "backgroundColor", figma: "#ff0000", rendered: "#ff0000", match: true },
57
+ { property: "color", figma: "#0000ff", rendered: "#0000ff", match: true },
58
+ ];
59
+
60
+ const result = generateTokenPatches("Button", styleDiffs, registry, {
61
+ sourceFile: "src/Button.tsx",
62
+ });
63
+
64
+ expect(result.summary).toBeDefined();
65
+ expect(result.fixableCount).toBeGreaterThanOrEqual(0);
66
+ });
67
+
68
+ it("returns empty patches when all values already use tokens", () => {
69
+ const styleDiffs = [
70
+ { property: "backgroundColor", figma: "var(--color-primary)", rendered: "var(--color-primary)", match: true },
71
+ ];
72
+
73
+ const result = generateTokenPatches("Button", styleDiffs, registry);
74
+
75
+ // All values are already tokens
76
+ expect(result.summary).toContain("compliance");
77
+ });
78
+
79
+ it("identifies unfixable values without matching tokens", () => {
80
+ const styleDiffs = [
81
+ { property: "backgroundColor", figma: "#abc123", rendered: "#abc123", match: true },
82
+ ];
83
+
84
+ const result = generateTokenPatches("Button", styleDiffs, registry);
85
+
86
+ expect(result.unfixableCount).toBeGreaterThanOrEqual(0);
87
+ });
88
+
89
+ it("includes summary with fix counts", () => {
90
+ const styleDiffs = [
91
+ { property: "padding", figma: "16px", rendered: "16px", match: true },
92
+ ];
93
+
94
+ const result = generateTokenPatches("Card", styleDiffs, registry);
95
+
96
+ expect(result.summary).toBeDefined();
97
+ expect(typeof result.fixableCount).toBe("number");
98
+ expect(typeof result.unfixableCount).toBe("number");
99
+ });
100
+ });
101
+
102
+ describe("generateCSSInJSPatches", () => {
103
+ it("generates patches for inline style values", () => {
104
+ const source = `
105
+ function Button() {
106
+ return <button style={{ backgroundColor: "#ff0000", padding: "16px" }}>Click</button>;
107
+ }
108
+ `;
109
+
110
+ const fixes = [
111
+ { property: "backgroundColor", currentValue: "#ff0000", tokenName: "color-primary" },
112
+ { property: "padding", currentValue: "16px", tokenName: "spacing-md" },
113
+ ];
114
+
115
+ const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
116
+
117
+ expect(patches.length).toBeGreaterThanOrEqual(0);
118
+ });
119
+
120
+ it("generates patches for styled-components template literals", () => {
121
+ const source = `
122
+ const Button = styled.button\`
123
+ background-color: #ff0000;
124
+ padding: 16px;
125
+ \`;
126
+ `;
127
+
128
+ const fixes = [
129
+ { property: "background-color", currentValue: "#ff0000", tokenName: "color-primary" },
130
+ { property: "padding", currentValue: "16px", tokenName: "spacing-md" },
131
+ ];
132
+
133
+ const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
134
+
135
+ expect(patches.length).toBeGreaterThanOrEqual(0);
136
+ });
137
+
138
+ it("returns empty patches for values not found in source", () => {
139
+ const source = `
140
+ function Button() {
141
+ return <button className="btn">Click</button>;
142
+ }
143
+ `;
144
+
145
+ const fixes = [
146
+ { property: "backgroundColor", currentValue: "#123456", tokenName: "color-unknown" },
147
+ ];
148
+
149
+ const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
150
+
151
+ expect(patches).toHaveLength(0);
152
+ });
153
+
154
+ it("handles camelCase property names", () => {
155
+ const source = `
156
+ function Button() {
157
+ return <button style={{ backgroundColor: "red" }}>Click</button>;
158
+ }
159
+ `;
160
+
161
+ const fixes = [
162
+ { property: "backgroundColor", currentValue: "red", tokenName: "color-error" },
163
+ ];
164
+
165
+ const patches = generateCSSInJSPatches(source, fixes, { fileName: "Button.tsx" });
166
+
167
+ expect(patches.length).toBeGreaterThanOrEqual(0);
168
+ });
169
+
170
+ it("handles kebab-case property names in CSS", () => {
171
+ const source = `
172
+ .button {
173
+ background-color: #ff0000;
174
+ }
175
+ `;
176
+
177
+ const fixes = [
178
+ { property: "background-color", currentValue: "#ff0000", tokenName: "color-primary" },
179
+ ];
180
+
181
+ const patches = generateCSSInJSPatches(source, fixes, { fileName: "styles.css" });
182
+
183
+ expect(patches.length).toBeGreaterThanOrEqual(0);
184
+ });
185
+ });
186
+ });