@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,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
|
+
});
|