@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,365 @@
1
+ /**
2
+ * Tests for the TypeScript props extractor module
3
+ */
4
+
5
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
6
+ import { mkdtemp, writeFile, rm, mkdir } from "node:fs/promises";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import {
10
+ extractPropsFromSource,
11
+ extractPropsFromFile,
12
+ convertToSegmentProps,
13
+ } from "../enhance/props-extractor.js";
14
+
15
+ let tempDir: string;
16
+
17
+ beforeAll(async () => {
18
+ tempDir = await mkdtemp(join(tmpdir(), "props-extractor-test-"));
19
+ });
20
+
21
+ afterAll(async () => {
22
+ await rm(tempDir, { recursive: true, force: true });
23
+ });
24
+
25
+ async function writeTestFile(name: string, content: string): Promise<string> {
26
+ const filePath = join(tempDir, name);
27
+ await writeFile(filePath, content, "utf-8");
28
+ return filePath;
29
+ }
30
+
31
+ describe("extractPropsFromSource", () => {
32
+ it("should extract props from a basic interface", () => {
33
+ const source = `
34
+ interface ButtonProps {
35
+ /** The visual style of the button */
36
+ variant: "primary" | "secondary" | "tertiary";
37
+ /** Button label text */
38
+ label: string;
39
+ /** Whether the button is disabled */
40
+ disabled?: boolean;
41
+ }
42
+
43
+ export function Button(props: ButtonProps) {
44
+ return <button>{props.label}</button>;
45
+ }
46
+ `;
47
+ const result = extractPropsFromSource(source, "Button.tsx");
48
+
49
+ expect(result.success).toBe(true);
50
+ expect(result.props).toHaveLength(3);
51
+
52
+ const variantProp = result.props.find((p) => p.name === "variant");
53
+ expect(variantProp).toBeDefined();
54
+ expect(variantProp?.required).toBe(true);
55
+ expect(variantProp?.description).toBe("The visual style of the button");
56
+ expect(variantProp?.enumValues).toEqual(["primary", "secondary", "tertiary"]);
57
+
58
+ const labelProp = result.props.find((p) => p.name === "label");
59
+ expect(labelProp).toBeDefined();
60
+ expect(labelProp?.type).toBe("string");
61
+ expect(labelProp?.required).toBe(true);
62
+
63
+ const disabledProp = result.props.find((p) => p.name === "disabled");
64
+ expect(disabledProp).toBeDefined();
65
+ expect(disabledProp?.required).toBe(false);
66
+ expect(disabledProp?.propType.type).toBe("boolean");
67
+ });
68
+
69
+ it("should extract props from a type alias", () => {
70
+ const source = `
71
+ type InputProps = {
72
+ /** Input type */
73
+ type: "text" | "email" | "password";
74
+ /** Input value */
75
+ value: string;
76
+ /** Placeholder text */
77
+ placeholder?: string;
78
+ };
79
+
80
+ export const Input = (props: InputProps) => <input {...props} />;
81
+ `;
82
+ const result = extractPropsFromSource(source, "Input.tsx");
83
+
84
+ expect(result.success).toBe(true);
85
+ expect(result.props).toHaveLength(3);
86
+
87
+ const typeProp = result.props.find((p) => p.name === "type");
88
+ expect(typeProp?.enumValues).toEqual(["text", "email", "password"]);
89
+ });
90
+
91
+ it("should handle function types", () => {
92
+ const source = `
93
+ interface ClickableProps {
94
+ /** Click handler */
95
+ onClick: (event: MouseEvent) => void;
96
+ /** Optional callback */
97
+ onHover?: () => void;
98
+ }
99
+
100
+ export function Clickable(props: ClickableProps) {
101
+ return <div />;
102
+ }
103
+ `;
104
+ const result = extractPropsFromSource(source, "Clickable.tsx");
105
+
106
+ expect(result.success).toBe(true);
107
+ expect(result.props).toHaveLength(2);
108
+
109
+ const onClickProp = result.props.find((p) => p.name === "onClick");
110
+ expect(onClickProp).toBeDefined();
111
+ expect(onClickProp?.propType.type).toBe("function");
112
+ expect(onClickProp?.required).toBe(true);
113
+ });
114
+
115
+ it("should handle array types", () => {
116
+ const source = `
117
+ interface ListProps {
118
+ /** List items */
119
+ items: string[];
120
+ /** Selected items */
121
+ selected?: number[];
122
+ }
123
+
124
+ export function List(props: ListProps) {
125
+ return <ul />;
126
+ }
127
+ `;
128
+ const result = extractPropsFromSource(source, "List.tsx");
129
+
130
+ expect(result.success).toBe(true);
131
+ expect(result.props).toHaveLength(2);
132
+
133
+ const itemsProp = result.props.find((p) => p.name === "items");
134
+ expect(itemsProp?.propType.type).toBe("array");
135
+ });
136
+
137
+ it("should handle ReactNode and children", () => {
138
+ const source = `
139
+ import { ReactNode } from "react";
140
+
141
+ interface ContainerProps {
142
+ /** Child elements */
143
+ children: ReactNode;
144
+ /** Optional header */
145
+ header?: ReactNode;
146
+ }
147
+
148
+ export function Container(props: ContainerProps) {
149
+ return <div>{props.children}</div>;
150
+ }
151
+ `;
152
+ const result = extractPropsFromSource(source, "Container.tsx");
153
+
154
+ expect(result.success).toBe(true);
155
+ expect(result.props).toHaveLength(2);
156
+
157
+ const childrenProp = result.props.find((p) => p.name === "children");
158
+ expect(childrenProp?.propType.type).toBe("node");
159
+ });
160
+
161
+ it("should extract JSDoc default values", () => {
162
+ const source = `
163
+ interface PaginationProps {
164
+ /** Current page number
165
+ * @default 1
166
+ */
167
+ page?: number;
168
+ /** Items per page
169
+ * @default 10
170
+ */
171
+ pageSize?: number;
172
+ }
173
+
174
+ export function Pagination(props: PaginationProps) {
175
+ return <div />;
176
+ }
177
+ `;
178
+ const result = extractPropsFromSource(source, "Pagination.tsx");
179
+
180
+ expect(result.success).toBe(true);
181
+
182
+ const pageProp = result.props.find((p) => p.name === "page");
183
+ expect(pageProp?.defaultValue).toBe(1);
184
+
185
+ const pageSizeProp = result.props.find((p) => p.name === "pageSize");
186
+ expect(pageSizeProp?.defaultValue).toBe(10);
187
+ });
188
+
189
+ it("should detect deprecated props", () => {
190
+ const source = `
191
+ interface CardProps {
192
+ /** Card title */
193
+ title: string;
194
+ /**
195
+ * Old subtitle prop
196
+ * @deprecated Use description instead
197
+ */
198
+ subtitle?: string;
199
+ /** Card description */
200
+ description?: string;
201
+ }
202
+
203
+ export function Card(props: CardProps) {
204
+ return <div />;
205
+ }
206
+ `;
207
+ const result = extractPropsFromSource(source, "Card.tsx");
208
+
209
+ expect(result.success).toBe(true);
210
+
211
+ const subtitleProp = result.props.find((p) => p.name === "subtitle");
212
+ expect(subtitleProp?.deprecated).toBe("Use description instead");
213
+ });
214
+
215
+ it("should handle number and boolean literals in unions", () => {
216
+ const source = `
217
+ interface GridProps {
218
+ /** Number of columns */
219
+ columns: 1 | 2 | 3 | 4 | 6 | 12;
220
+ /** Spacing variant */
221
+ spacing: "none" | "small" | "medium" | "large";
222
+ }
223
+
224
+ export function Grid(props: GridProps) {
225
+ return <div />;
226
+ }
227
+ `;
228
+ const result = extractPropsFromSource(source, "Grid.tsx");
229
+
230
+ expect(result.success).toBe(true);
231
+
232
+ const spacingProp = result.props.find((p) => p.name === "spacing");
233
+ expect(spacingProp?.enumValues).toEqual(["none", "small", "medium", "large"]);
234
+ });
235
+
236
+ it("should skip internal props starting with _ or $", () => {
237
+ const source = `
238
+ interface InternalProps {
239
+ /** Public prop */
240
+ visible: boolean;
241
+ /** Internal state */
242
+ _internal: number;
243
+ /** Another internal */
244
+ $secret: string;
245
+ }
246
+
247
+ export function Internal(props: InternalProps) {
248
+ return <div />;
249
+ }
250
+ `;
251
+ const result = extractPropsFromSource(source, "Internal.tsx");
252
+
253
+ expect(result.success).toBe(true);
254
+ expect(result.props).toHaveLength(1);
255
+ expect(result.props[0].name).toBe("visible");
256
+ });
257
+
258
+ it("should find any Props interface if named one not found", () => {
259
+ const source = `
260
+ interface MyCustomProps {
261
+ /** Custom value */
262
+ value: string;
263
+ }
264
+
265
+ export function Component(props: MyCustomProps) {
266
+ return <div />;
267
+ }
268
+ `;
269
+ const result = extractPropsFromSource(source, "Unknown.tsx", {
270
+ propsTypeName: "UnknownProps", // Won't be found
271
+ });
272
+
273
+ expect(result.success).toBe(true);
274
+ expect(result.props).toHaveLength(1);
275
+ expect(result.propsTypeName).toBe("MyCustomProps");
276
+ });
277
+ });
278
+
279
+ describe("extractPropsFromFile", () => {
280
+ it("should extract props from a file", async () => {
281
+ const filePath = await writeTestFile(
282
+ "FileButton.tsx",
283
+ `
284
+ interface FileButtonProps {
285
+ /** Button variant */
286
+ variant: "primary" | "secondary";
287
+ /** Button text */
288
+ children: React.ReactNode;
289
+ }
290
+
291
+ export function FileButton(props: FileButtonProps) {
292
+ return <button>{props.children}</button>;
293
+ }
294
+ `
295
+ );
296
+
297
+ const result = await extractPropsFromFile(filePath);
298
+
299
+ expect(result.success).toBe(true);
300
+ expect(result.componentName).toBe("FileButton");
301
+ expect(result.props).toHaveLength(2);
302
+ });
303
+
304
+ it("should handle non-existent files gracefully", async () => {
305
+ const result = await extractPropsFromFile("/nonexistent/path/Button.tsx");
306
+
307
+ expect(result.success).toBe(false);
308
+ expect(result.warnings.length).toBeGreaterThan(0);
309
+ });
310
+
311
+ it("should infer component name from index.tsx", async () => {
312
+ const compDir = join(tempDir, "MyComponent");
313
+ await mkdir(compDir, { recursive: true });
314
+
315
+ const filePath = await writeTestFile(
316
+ "MyComponent/index.tsx",
317
+ `
318
+ interface MyComponentProps {
319
+ value: string;
320
+ }
321
+
322
+ export function MyComponent(props: MyComponentProps) {
323
+ return <div />;
324
+ }
325
+ `
326
+ );
327
+
328
+ const result = await extractPropsFromFile(filePath);
329
+
330
+ expect(result.componentName).toBe("MyComponent");
331
+ });
332
+ });
333
+
334
+ describe("convertToSegmentProps", () => {
335
+ it("should convert extracted props to segment format", () => {
336
+ const source = `
337
+ interface ButtonProps {
338
+ /** Button variant */
339
+ variant: "primary" | "secondary";
340
+ /** Whether disabled
341
+ * @default false
342
+ */
343
+ disabled?: boolean;
344
+ /** Click handler */
345
+ onClick: () => void;
346
+ }
347
+ `;
348
+ const result = extractPropsFromSource(source, "Button.tsx");
349
+ const segmentProps = convertToSegmentProps(result.props);
350
+
351
+ expect(segmentProps).toHaveProperty("variant");
352
+ expect(segmentProps.variant.type).toBe("enum");
353
+ expect(segmentProps.variant.values).toEqual(["primary", "secondary"]);
354
+ expect(segmentProps.variant.description).toBe("Button variant");
355
+
356
+ expect(segmentProps).toHaveProperty("disabled");
357
+ expect(segmentProps.disabled.type).toBe("boolean");
358
+ expect(segmentProps.disabled.required).toBe(false);
359
+ expect(segmentProps.disabled.default).toBe(false);
360
+
361
+ expect(segmentProps).toHaveProperty("onClick");
362
+ expect(segmentProps.onClick.type).toBe("function");
363
+ expect(segmentProps.onClick.required).toBe(true);
364
+ });
365
+ });
@@ -0,0 +1,267 @@
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
+ TokenRegistryManager,
7
+ createTokenRegistry,
8
+ } from "../token-registry.js";
9
+ import type { TokenConfig } from "../../core/index.js";
10
+
11
+ describe("TokenRegistryManager", () => {
12
+ let testDir: string;
13
+ let manager: TokenRegistryManager;
14
+
15
+ beforeEach(async () => {
16
+ testDir = join(tmpdir(), `token-registry-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
17
+ await mkdir(testDir, { recursive: true });
18
+ manager = createTokenRegistry();
19
+ });
20
+
21
+ afterEach(async () => {
22
+ manager.clear();
23
+ try {
24
+ await rm(testDir, { recursive: true, force: true });
25
+ } catch {
26
+ // Ignore cleanup errors
27
+ }
28
+ });
29
+
30
+ describe("findClosestToken", () => {
31
+ beforeEach(async () => {
32
+ // Create a CSS token file for testing
33
+ const cssTokens = `
34
+ :root {
35
+ --color-primary: #ff0000;
36
+ --color-secondary: #0000ff;
37
+ --color-accent: #ff0044;
38
+ --color-gray: #808080;
39
+ --spacing-xs: 4px;
40
+ --spacing-sm: 8px;
41
+ --spacing-md: 16px;
42
+ --spacing-lg: 24px;
43
+ --spacing-xl: 32px;
44
+ --sizing-icon: 24px;
45
+ --sizing-button: 40px;
46
+ --radius-sm: 4px;
47
+ --radius-md: 8px;
48
+ --radius-lg: 16px;
49
+ --radius-full: 9999px;
50
+ }
51
+ `;
52
+
53
+ await writeFile(join(testDir, "tokens.css"), cssTokens);
54
+
55
+ const config: TokenConfig = {
56
+ include: ["tokens.css"],
57
+ };
58
+
59
+ await manager.initialize(config, testDir);
60
+ });
61
+
62
+ describe("numeric values", () => {
63
+ it("finds exact matching token", () => {
64
+ const results = manager.findClosestToken("16px", "spacing");
65
+
66
+ expect(results.length).toBeGreaterThan(0);
67
+ expect(results[0].token.name).toContain("spacing");
68
+ expect(results[0].token.name).toContain("md");
69
+ expect(results[0].distance).toBe(0);
70
+ expect(results[0].confidence).toBe(1);
71
+ });
72
+
73
+ it("finds close numeric tokens within threshold", () => {
74
+ // 17px is 1px away from 16px
75
+ const results = manager.findClosestToken("17px", "spacing", {
76
+ numericThreshold: 4,
77
+ });
78
+
79
+ expect(results.length).toBeGreaterThan(0);
80
+ expect(results[0].token.name).toContain("md"); // 16px is closest
81
+ expect(results[0].distance).toBe(1);
82
+ });
83
+
84
+ it("excludes tokens outside threshold", () => {
85
+ // 50px is far from all spacing tokens (closest is 32px = xl, 18px away)
86
+ const results = manager.findClosestToken("50px", "spacing", {
87
+ numericThreshold: 4,
88
+ });
89
+
90
+ // Should return empty since nothing is within 4px
91
+ expect(results).toHaveLength(0);
92
+ });
93
+
94
+ it("sorts results by distance", () => {
95
+ // 20px: between md (16px, 4 away) and lg (24px, 4 away)
96
+ const results = manager.findClosestToken("20px", "spacing", {
97
+ numericThreshold: 5,
98
+ });
99
+
100
+ expect(results.length).toBeGreaterThanOrEqual(2);
101
+ // Results should be sorted by distance
102
+ for (let i = 1; i < results.length; i++) {
103
+ expect(results[i].distance).toBeGreaterThanOrEqual(results[i - 1].distance);
104
+ }
105
+ });
106
+
107
+ it("infers category from numeric value when not specified", () => {
108
+ const results = manager.findClosestToken("16px");
109
+
110
+ // Should search spacing, sizing, and radius
111
+ expect(results.length).toBeGreaterThan(0);
112
+ });
113
+ });
114
+
115
+ describe("color values", () => {
116
+ it("finds exact matching color token (hex)", () => {
117
+ const results = manager.findClosestToken("#ff0000", "color");
118
+
119
+ expect(results.length).toBeGreaterThan(0);
120
+ expect(results[0].token.name).toContain("color");
121
+ expect(results[0].token.name).toContain("primary");
122
+ expect(results[0].distance).toBe(0);
123
+ expect(results[0].confidence).toBe(1);
124
+ });
125
+
126
+ it("finds exact matching color token (case insensitive)", () => {
127
+ const results = manager.findClosestToken("#FF0000", "color");
128
+
129
+ expect(results.length).toBeGreaterThan(0);
130
+ expect(results[0].token.name).toContain("primary");
131
+ });
132
+
133
+ it("finds close colors within Delta E threshold", () => {
134
+ // #ff0022 is close to #ff0000 (primary) and #ff0044 (accent)
135
+ const results = manager.findClosestToken("#ff0022", "color", {
136
+ colorDeltaE: 15,
137
+ });
138
+
139
+ // Should find at least one close match
140
+ expect(results.length).toBeGreaterThanOrEqual(0);
141
+ });
142
+
143
+ it("infers color category when not specified", () => {
144
+ const results = manager.findClosestToken("#ff0000");
145
+
146
+ expect(results.length).toBeGreaterThan(0);
147
+ expect(results[0].token.category).toBe("color");
148
+ });
149
+ });
150
+
151
+ describe("options", () => {
152
+ it("respects limit option", () => {
153
+ const results = manager.findClosestToken("10px", "spacing", {
154
+ numericThreshold: 10,
155
+ limit: 2,
156
+ });
157
+
158
+ expect(results.length).toBeLessThanOrEqual(2);
159
+ });
160
+
161
+ it("returns empty array when no tokens match", () => {
162
+ const results = manager.findClosestToken("100px", "spacing", {
163
+ numericThreshold: 2,
164
+ });
165
+
166
+ expect(results).toHaveLength(0);
167
+ });
168
+
169
+ it("returns empty when registry not initialized", () => {
170
+ const uninitializedManager = createTokenRegistry();
171
+ const results = uninitializedManager.findClosestToken("#ff0000", "color");
172
+
173
+ expect(results).toHaveLength(0);
174
+ });
175
+ });
176
+
177
+ describe("confidence scores", () => {
178
+ it("returns confidence of 1 for exact matches", () => {
179
+ const results = manager.findClosestToken("16px", "spacing");
180
+
181
+ expect(results[0].confidence).toBe(1);
182
+ });
183
+
184
+ it("returns lower confidence for non-exact matches", () => {
185
+ const results = manager.findClosestToken("18px", "spacing", {
186
+ numericThreshold: 4,
187
+ });
188
+
189
+ expect(results[0].confidence).toBeLessThan(1);
190
+ expect(results[0].confidence).toBeGreaterThan(0);
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("initialization", () => {
196
+ it("can be initialized with token files", async () => {
197
+ const cssTokens = `:root { --color-test: #123456; }`;
198
+ await writeFile(join(testDir, "test-tokens.css"), cssTokens);
199
+
200
+ const config: TokenConfig = {
201
+ include: ["test-tokens.css"],
202
+ };
203
+
204
+ const registry = await manager.initialize(config, testDir);
205
+
206
+ expect(registry).toBeDefined();
207
+ expect(manager.isInitialized()).toBe(true);
208
+ });
209
+
210
+ it("returns existing registry if already initialized", async () => {
211
+ const cssTokens = `:root { --color-test: #123456; }`;
212
+ await writeFile(join(testDir, "tokens.css"), cssTokens);
213
+
214
+ const config: TokenConfig = {
215
+ include: ["tokens.css"],
216
+ };
217
+
218
+ const registry1 = await manager.initialize(config, testDir);
219
+ const registry2 = await manager.initialize(config, testDir);
220
+
221
+ expect(registry1).toBe(registry2);
222
+ });
223
+ });
224
+
225
+ describe("findByValue", () => {
226
+ beforeEach(async () => {
227
+ const cssTokens = `:root { --color-primary: #ff0000; }`;
228
+ await writeFile(join(testDir, "tokens.css"), cssTokens);
229
+
230
+ const config: TokenConfig = {
231
+ include: ["tokens.css"],
232
+ };
233
+
234
+ await manager.initialize(config, testDir);
235
+ });
236
+
237
+ it("finds token by exact value", () => {
238
+ const names = manager.findByValue("#ff0000");
239
+
240
+ expect(names.length).toBeGreaterThan(0);
241
+ expect(names.some(n => n.includes("primary"))).toBe(true);
242
+ });
243
+
244
+ it("returns empty array for unknown value", () => {
245
+ const names = manager.findByValue("#ffffff");
246
+
247
+ expect(names).toHaveLength(0);
248
+ });
249
+ });
250
+
251
+ describe("clear", () => {
252
+ it("clears the registry", async () => {
253
+ const cssTokens = `:root { --color-test: #123456; }`;
254
+ await writeFile(join(testDir, "tokens.css"), cssTokens);
255
+
256
+ const config: TokenConfig = {
257
+ include: ["tokens.css"],
258
+ };
259
+
260
+ await manager.initialize(config, testDir);
261
+ expect(manager.isInitialized()).toBe(true);
262
+
263
+ manager.clear();
264
+ expect(manager.isInitialized()).toBe(false);
265
+ });
266
+ });
267
+ });