@fragments-sdk/cli 0.9.1 → 0.10.1

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 (108) hide show
  1. package/dist/bin.d.ts +1 -0
  2. package/dist/bin.js +435 -67
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-D7372LQX.js → chunk-5G3VZH43.js} +8 -12
  5. package/dist/chunk-5G3VZH43.js.map +1 -0
  6. package/dist/chunk-D2CDBRNU.js +2 -0
  7. package/dist/{chunk-YMPGYEWK.js → chunk-D5PYOXEI.js} +2 -2
  8. package/dist/{chunk-BW3ZATBW.js → chunk-HRFUSSZI.js} +27 -10
  9. package/dist/chunk-HRFUSSZI.js.map +1 -0
  10. package/dist/{chunk-GF6OVPIN.js → chunk-OQO55NKV.js} +405 -34
  11. package/dist/chunk-OQO55NKV.js.map +1 -0
  12. package/dist/{chunk-TOIE7VXF.js → chunk-PW7QTQA6.js} +2 -2
  13. package/dist/{chunk-AWYCDRPG.js → chunk-WXSR2II7.js} +2 -2
  14. package/dist/chunk-WXSR2II7.js.map +1 -0
  15. package/dist/{chunk-5GT62FCB.js → chunk-ZM4ZQZWZ.js} +5 -5
  16. package/dist/core/index.d.ts +1 -2194
  17. package/dist/core/index.js +22 -27
  18. package/dist/{discovery-Z4RDDFVR.js → discovery-NEOY4MPN.js} +3 -3
  19. package/dist/{generate-LQA2R7FN.js → generate-FBHSXR3D.js} +5 -7
  20. package/dist/{generate-LQA2R7FN.js.map → generate-FBHSXR3D.js.map} +1 -1
  21. package/dist/index.d.ts +3 -5
  22. package/dist/index.js +7 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/{init-2GEGVIUQ.js → init-NDQXUWDU.js} +58 -6
  25. package/dist/init-NDQXUWDU.js.map +1 -0
  26. package/dist/mcp-bin.js +5 -8
  27. package/dist/mcp-bin.js.map +1 -1
  28. package/dist/scan-CJF2DOQW.js +14 -0
  29. package/dist/scan-generate-SJAN5MVI.js +691 -0
  30. package/dist/scan-generate-SJAN5MVI.js.map +1 -0
  31. package/dist/{service-XP2EAJXD.js → service-TQYWY65E.js} +4 -6
  32. package/dist/{static-viewer-XCS7UJTO.js → static-viewer-NUBFPKWH.js} +4 -6
  33. package/dist/{test-TD6TJNVY.js → test-Z5LVO724.js} +4 -5
  34. package/dist/{test-TD6TJNVY.js.map → test-Z5LVO724.js.map} +1 -1
  35. package/dist/{tokens-2EXPCVP3.js → tokens-CE46OTMD.js} +6 -8
  36. package/dist/{tokens-2EXPCVP3.js.map → tokens-CE46OTMD.js.map} +1 -1
  37. package/dist/{viewer-RFA2KVBG.js → viewer-DNMNC5VS.js} +16 -19
  38. package/dist/viewer-DNMNC5VS.js.map +1 -0
  39. package/package.json +2 -1
  40. package/src/bin.ts +33 -1
  41. package/src/build.ts +1 -1
  42. package/src/commands/__tests__/scan-generate.test.ts +308 -0
  43. package/src/commands/init.ts +72 -5
  44. package/src/commands/perf.ts +1 -1
  45. package/src/commands/scan-generate.ts +1013 -0
  46. package/src/commands/setup.ts +499 -0
  47. package/src/core/auto-props.ts +1 -1
  48. package/src/core/bundle-measurer.ts +2 -2
  49. package/src/core/config.ts +2 -3
  50. package/src/core/discovery.ts +2 -2
  51. package/src/core/generators/context.ts +1 -1
  52. package/src/core/generators/registry.ts +3 -3
  53. package/src/core/generators/typescript-extractor.ts +1 -1
  54. package/src/core/graph-extractor.ts +1 -1
  55. package/src/core/index.ts +3 -205
  56. package/src/core/loader.ts +40 -10
  57. package/src/core/parser.ts +1 -1
  58. package/src/core/previewLoader.ts +1 -1
  59. package/src/index.ts +2 -2
  60. package/src/service/snippet-validation.test.ts +1 -1
  61. package/src/service/snippet-validation.ts +2 -2
  62. package/src/viewer/__tests__/viewer-integration.test.ts +3 -9
  63. package/src/viewer/assets/fragments_logo.png +0 -0
  64. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +2 -10
  65. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +4 -1
  66. package/src/viewer/vite-plugin.ts +1 -1
  67. package/dist/chunk-AWYCDRPG.js.map +0 -1
  68. package/dist/chunk-BW3ZATBW.js.map +0 -1
  69. package/dist/chunk-D7372LQX.js.map +0 -1
  70. package/dist/chunk-EKLMXTWU.js +0 -80
  71. package/dist/chunk-EKLMXTWU.js.map +0 -1
  72. package/dist/chunk-EZYXYWNF.js +0 -131
  73. package/dist/chunk-EZYXYWNF.js.map +0 -1
  74. package/dist/chunk-GF6OVPIN.js.map +0 -1
  75. package/dist/chunk-NVSPGSKB.js +0 -203
  76. package/dist/chunk-NVSPGSKB.js.map +0 -1
  77. package/dist/defineFragment-CBMS7Bab.d.ts +0 -685
  78. package/dist/init-2GEGVIUQ.js.map +0 -1
  79. package/dist/scan-JGS65S7P.js +0 -16
  80. package/dist/storyFilters-3LUYAFZF.js +0 -15
  81. package/dist/viewer-RFA2KVBG.js.map +0 -1
  82. package/src/core/__tests__/preview-runtime.test.tsx +0 -111
  83. package/src/core/composition.test.ts +0 -262
  84. package/src/core/composition.ts +0 -318
  85. package/src/core/constants.ts +0 -114
  86. package/src/core/context.ts +0 -2
  87. package/src/core/defineFragment.ts +0 -141
  88. package/src/core/figma.ts +0 -263
  89. package/src/core/fragment-types.ts +0 -214
  90. package/src/core/performance-presets.ts +0 -142
  91. package/src/core/preview-runtime.tsx +0 -144
  92. package/src/core/schema.ts +0 -229
  93. package/src/core/storyAdapter.test.ts +0 -571
  94. package/src/core/storyAdapter.ts +0 -761
  95. package/src/core/storyFilters.test.ts +0 -350
  96. package/src/core/storyFilters.ts +0 -253
  97. package/src/core/storybook-csf.ts +0 -11
  98. package/src/core/token-parser.ts +0 -321
  99. package/src/core/token-types.ts +0 -287
  100. package/src/core/types.ts +0 -784
  101. /package/dist/{discovery-Z4RDDFVR.js.map → chunk-D2CDBRNU.js.map} +0 -0
  102. /package/dist/{chunk-YMPGYEWK.js.map → chunk-D5PYOXEI.js.map} +0 -0
  103. /package/dist/{chunk-TOIE7VXF.js.map → chunk-PW7QTQA6.js.map} +0 -0
  104. /package/dist/{chunk-5GT62FCB.js.map → chunk-ZM4ZQZWZ.js.map} +0 -0
  105. /package/dist/{scan-JGS65S7P.js.map → discovery-NEOY4MPN.js.map} +0 -0
  106. /package/dist/{service-XP2EAJXD.js.map → scan-CJF2DOQW.js.map} +0 -0
  107. /package/dist/{static-viewer-XCS7UJTO.js.map → service-TQYWY65E.js.map} +0 -0
  108. /package/dist/{storyFilters-3LUYAFZF.js.map → static-viewer-NUBFPKWH.js.map} +0 -0
@@ -0,0 +1,308 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ extractComponentJSDocFromSource,
7
+ detectCompoundComponentsFromSource,
8
+ calculateFieldConfidence,
9
+ scanGenerate,
10
+ } from "../scan-generate.js";
11
+ import type { ExtractedProp } from "../../service/enhance/props-extractor.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // JSDoc Extraction
15
+ // ---------------------------------------------------------------------------
16
+
17
+ describe("extractComponentJSDocFromSource", () => {
18
+ it("extracts JSDoc from an exported function declaration", () => {
19
+ const source = `
20
+ /** A beautiful button component for user interactions. */
21
+ export function Button(props: ButtonProps) {
22
+ return <button>{props.children}</button>;
23
+ }
24
+ `;
25
+ const result = extractComponentJSDocFromSource(source, "Button.tsx", "Button");
26
+ expect(result).toBe(
27
+ "A beautiful button component for user interactions."
28
+ );
29
+ });
30
+
31
+ it("extracts JSDoc from an exported const arrow function", () => {
32
+ const source = `
33
+ /** Card container for grouping related content. */
34
+ export const Card = (props: CardProps) => {
35
+ return <div>{props.children}</div>;
36
+ };
37
+ `;
38
+ const result = extractComponentJSDocFromSource(source, "Card.tsx", "Card");
39
+ expect(result).toBe("Card container for grouping related content.");
40
+ });
41
+
42
+ it("returns null when there is no JSDoc", () => {
43
+ const source = `
44
+ export function Button(props: ButtonProps) {
45
+ return <button>{props.children}</button>;
46
+ }
47
+ `;
48
+ const result = extractComponentJSDocFromSource(source, "Button.tsx", "Button");
49
+ expect(result).toBeNull();
50
+ });
51
+
52
+ it("stops at JSDoc tags and only returns the description", () => {
53
+ const source = `
54
+ /**
55
+ * A toggle switch component.
56
+ * @param props - The switch props
57
+ * @example <Switch checked />
58
+ */
59
+ export function Switch(props: SwitchProps) {
60
+ return <input type="checkbox" />;
61
+ }
62
+ `;
63
+ const result = extractComponentJSDocFromSource(source, "Switch.tsx", "Switch");
64
+ expect(result).toBe("A toggle switch component.");
65
+ });
66
+
67
+ it("extracts JSDoc from a default export function", () => {
68
+ const source = `
69
+ /** The main App component. */
70
+ export default function App() {
71
+ return <div />;
72
+ }
73
+ `;
74
+ const result = extractComponentJSDocFromSource(source, "App.tsx");
75
+ expect(result).toBe("The main App component.");
76
+ });
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Compound Component Detection
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe("detectCompoundComponentsFromSource", () => {
84
+ it("detects Object.assign with shorthand properties", () => {
85
+ const source = `
86
+ const Root = () => <div />;
87
+ const Header = () => <header />;
88
+ const Body = () => <main />;
89
+ const Footer = () => <footer />;
90
+
91
+ export const Card = Object.assign(Root, { Header, Body, Footer });
92
+ `;
93
+ const result = detectCompoundComponentsFromSource(source, "Card.tsx");
94
+ expect(result).toEqual(["Header", "Body", "Footer"]);
95
+ });
96
+
97
+ it("detects Object.assign with renamed properties", () => {
98
+ const source = `
99
+ const CardRoot = () => <div />;
100
+ const CardHeader = () => <header />;
101
+
102
+ export const Card = Object.assign(CardRoot, { Header: CardHeader });
103
+ `;
104
+ const result = detectCompoundComponentsFromSource(source, "Card.tsx");
105
+ expect(result).toEqual(["Header"]);
106
+ });
107
+
108
+ it("returns empty array when no compound pattern found", () => {
109
+ const source = `
110
+ export function Button() {
111
+ return <button />;
112
+ }
113
+ `;
114
+ const result = detectCompoundComponentsFromSource(source, "Button.tsx");
115
+ expect(result).toEqual([]);
116
+ });
117
+ });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Confidence Scoring
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe("calculateFieldConfidence", () => {
124
+ const makeProps = (names: string[]): ExtractedProp[] =>
125
+ names.map((name) => ({
126
+ name,
127
+ type: "string",
128
+ propType: { type: "string" as const },
129
+ description: "",
130
+ required: false,
131
+ }));
132
+
133
+ it("gives +30 for extracted props", () => {
134
+ const result = calculateFieldConfidence({
135
+ component: { name: "Foo", sourcePath: "/foo.tsx", relativePath: "foo.tsx" },
136
+ props: {
137
+ filePath: "/foo.tsx",
138
+ componentName: "Foo",
139
+ props: makeProps(["children"]),
140
+ success: true,
141
+ warnings: [],
142
+ },
143
+ jsDoc: null,
144
+ compoundChildren: [],
145
+ storyVariants: [],
146
+ });
147
+ // +30 (props) + 0 (no JSDoc → TODO) + 0 (category fallback → TODO) + 0 (no stories) + 10 (all resolved) + 0 (no defaults) = 40
148
+ // But "children" prop → category "Layout" (+10)
149
+ // So: 30 + 10 + 10 = 50
150
+ expect(result.score).toBe(50);
151
+ });
152
+
153
+ it("gives +15 for JSDoc description", () => {
154
+ const result = calculateFieldConfidence({
155
+ component: { name: "Foo", sourcePath: "/foo.tsx", relativePath: "foo.tsx" },
156
+ props: null,
157
+ jsDoc: "A component",
158
+ compoundChildren: [],
159
+ storyVariants: [],
160
+ });
161
+ // +15 (JSDoc), no props, category fallback → TODO, no stories
162
+ expect(result.score).toBe(15);
163
+ expect(result.todoFields).not.toContain("meta.description");
164
+ expect(result.todoFields).toContain("meta.category");
165
+ });
166
+
167
+ it("gives +25 for story variants", () => {
168
+ const result = calculateFieldConfidence({
169
+ component: { name: "Foo", sourcePath: "/foo.tsx", relativePath: "foo.tsx" },
170
+ props: null,
171
+ jsDoc: null,
172
+ compoundChildren: [],
173
+ storyVariants: [{ name: "Primary", args: {} }],
174
+ });
175
+ // +25 (stories)
176
+ expect(result.score).toBe(25);
177
+ });
178
+
179
+ it("gives +5 for compound children", () => {
180
+ const result = calculateFieldConfidence({
181
+ component: { name: "Foo", sourcePath: "/foo.tsx", relativePath: "foo.tsx" },
182
+ props: null,
183
+ jsDoc: null,
184
+ compoundChildren: ["Header", "Body"],
185
+ storyVariants: [],
186
+ });
187
+ expect(result.score).toBe(5);
188
+ });
189
+
190
+ it("always includes usage.when and usage.whenNot in TODOs", () => {
191
+ const result = calculateFieldConfidence({
192
+ component: { name: "Button", sourcePath: "/button.tsx", relativePath: "button.tsx" },
193
+ props: {
194
+ filePath: "/button.tsx",
195
+ componentName: "Button",
196
+ props: makeProps(["onClick"]),
197
+ success: true,
198
+ warnings: [],
199
+ },
200
+ jsDoc: "A button",
201
+ compoundChildren: [],
202
+ storyVariants: [{ name: "Primary", args: {} }],
203
+ });
204
+ expect(result.todoFields).toContain("usage.when");
205
+ expect(result.todoFields).toContain("usage.whenNot");
206
+ });
207
+ });
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Integration: scanGenerate against a temp component
211
+ // ---------------------------------------------------------------------------
212
+
213
+ describe("scanGenerate integration", () => {
214
+ let tmpDir: string;
215
+
216
+ beforeAll(async () => {
217
+ tmpDir = await mkdtemp(join(tmpdir(), "scan-gen-test-"));
218
+
219
+ // Create a simple component structure
220
+ const compDir = join(tmpDir, "components", "Button");
221
+ await mkdir(compDir, { recursive: true });
222
+
223
+ await writeFile(
224
+ join(compDir, "Button.tsx"),
225
+ `import React from 'react';
226
+
227
+ /** A primary button for user interactions. */
228
+ export interface ButtonProps {
229
+ /** Button label */
230
+ children: React.ReactNode;
231
+ /** Visual variant */
232
+ variant?: 'primary' | 'secondary';
233
+ /** Disabled state */
234
+ disabled?: boolean;
235
+ /** Click handler */
236
+ onClick?: () => void;
237
+ }
238
+
239
+ export function Button({ children, variant = 'primary', disabled, onClick }: ButtonProps) {
240
+ return <button disabled={disabled} onClick={onClick}>{children}</button>;
241
+ }
242
+ `,
243
+ "utf-8"
244
+ );
245
+ });
246
+
247
+ afterAll(async () => {
248
+ await rm(tmpDir, { recursive: true, force: true });
249
+ });
250
+
251
+ it("generates a fragment file with correct structure", async () => {
252
+ const result = await scanGenerate({
253
+ scanPath: join(tmpDir, "components"),
254
+ force: true,
255
+ });
256
+
257
+ expect(result.success).toBe(true);
258
+ expect(result.generated.length).toBe(1);
259
+ expect(result.generated[0].name).toBe("Button");
260
+ expect(result.generated[0].confidence).toBeGreaterThan(0);
261
+
262
+ // Read the generated file and check structure
263
+ const { readFile: rf } = await import("node:fs/promises");
264
+ const content = await rf(
265
+ join(tmpDir, "components", "Button", "Button.fragment.tsx"),
266
+ "utf-8"
267
+ );
268
+
269
+ // Check import uses @fragments-sdk/core
270
+ expect(content).toContain(
271
+ "import { defineFragment } from '@fragments-sdk/core'"
272
+ );
273
+
274
+ // Check confidence header
275
+ expect(content).toMatch(
276
+ /\/\/ Auto-generated by fragments init --scan \| Confidence: \d+\/100/
277
+ );
278
+
279
+ // Check TODO markers exist
280
+ expect(content).toContain("// TODO:");
281
+
282
+ // Check component name in meta
283
+ expect(content).toContain("name: 'Button'");
284
+
285
+ // Check JSDoc was picked up in description
286
+ expect(content).toContain("A primary button for user interactions.");
287
+
288
+ // Check category was inferred (Button → Actions)
289
+ expect(content).toContain("category: 'Actions'");
290
+
291
+ // Check props block has content
292
+ expect(content).toContain("children:");
293
+ expect(content).toContain("variant:");
294
+ });
295
+
296
+ it("skips existing fragments without --force", async () => {
297
+ // First run already created the fragment
298
+ const result = await scanGenerate({
299
+ scanPath: join(tmpDir, "components"),
300
+ force: false,
301
+ });
302
+
303
+ expect(result.skipped.length).toBe(1);
304
+ expect(result.skipped[0].name).toBe("Button");
305
+ expect(result.skipped[0].reason).toBe("Fragment already exists");
306
+ expect(result.generated.length).toBe(0);
307
+ });
308
+ });
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * fragments init - Smart interactive initialization
3
3
  *
4
- * Handles three scenarios:
5
- * 1. Stories foundConfigure and load existing stories
6
- * 2. Components found (no stories) Auto-generate documentation
7
- * 3. Fresh project Guided setup with example component
4
+ * Handles four scenarios:
5
+ * 1. --scan <path>Scan external component library, generate fragment files
6
+ * 2. Stories found Configure and load existing stories
7
+ * 3. Components found (no stories) Auto-generate documentation
8
+ * 4. Fresh project → Guided setup with example component
8
9
  */
9
10
 
10
11
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
@@ -29,12 +30,14 @@ export interface InitOptions {
29
30
  yes?: boolean;
30
31
  /** Explicit framework override */
31
32
  framework?: string;
33
+ /** Path to scan for components (enables scan mode) */
34
+ scan?: string;
32
35
  }
33
36
 
34
37
  export interface InitResult {
35
38
  success: boolean;
36
39
  configPath?: string;
37
- scenario: "stories" | "components" | "fresh";
40
+ scenario: "stories" | "components" | "fresh" | "scan";
38
41
  storiesFound: number;
39
42
  componentsFound: number;
40
43
  errors: string[];
@@ -396,6 +399,70 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
396
399
  const projectRoot = resolve(options.projectRoot || process.cwd());
397
400
  const errors: string[] = [];
398
401
 
402
+ // Early return for scan mode — non-interactive
403
+ if (options.scan) {
404
+ const scanPath = resolve(projectRoot, options.scan);
405
+
406
+ // Verify scan path exists
407
+ try {
408
+ await access(scanPath);
409
+ } catch {
410
+ console.error(pc.red(`\nScan path not found: ${scanPath}\n`));
411
+ return {
412
+ success: false,
413
+ scenario: "scan",
414
+ storiesFound: 0,
415
+ componentsFound: 0,
416
+ errors: [`Scan path not found: ${scanPath}`],
417
+ };
418
+ }
419
+
420
+ // Run scan-generate
421
+ const { scanGenerate } = await import("./scan-generate.js");
422
+ const scanResult = await scanGenerate({
423
+ scanPath,
424
+ force: options.force,
425
+ verbose: true,
426
+ });
427
+
428
+ // Create config pointing at the scanned path
429
+ const relScanPath = relative(projectRoot, scanPath);
430
+ const configPath = join(projectRoot, BRAND.configFile);
431
+ const configContent = generateConfig({
432
+ includePaths: [`${relScanPath}/**/*.fragment.tsx`],
433
+ componentPaths: [`${relScanPath}/**/*.tsx`],
434
+ framework: "react",
435
+ });
436
+
437
+ try {
438
+ await writeFile(configPath, configContent, "utf-8");
439
+ console.log(pc.green(`✓ Created ${BRAND.configFile}`));
440
+ } catch (e) {
441
+ errors.push(`Failed to create config: ${e}`);
442
+ }
443
+
444
+ // Next steps
445
+ if (scanResult.success) {
446
+ console.log(pc.cyan("Next steps:"));
447
+ console.log(` 1. Search generated files for ${pc.bold("TODO:")} markers and fill in human knowledge`);
448
+ console.log(` 2. Run ${pc.bold(`${BRAND.cliCommand} dev`)} to preview your components`);
449
+ console.log(` 3. Run ${pc.bold(`${BRAND.cliCommand} build`)} to compile fragments.json`);
450
+ console.log();
451
+ }
452
+
453
+ return {
454
+ success: scanResult.success && errors.length === 0,
455
+ configPath: errors.length === 0 ? configPath : undefined,
456
+ scenario: "scan",
457
+ storiesFound: 0,
458
+ componentsFound: scanResult.generated.length,
459
+ errors: [
460
+ ...errors,
461
+ ...scanResult.errors.map((e) => `${e.name}: ${e.error}`),
462
+ ],
463
+ };
464
+ }
465
+
399
466
  console.log(pc.cyan(`\n✨ Welcome to ${BRAND.name}!\n`));
400
467
 
401
468
  // Step 1: Detect what exists
@@ -16,7 +16,7 @@ import {
16
16
  budgetBar,
17
17
  type PerformanceConfig,
18
18
  type ComplexityTier,
19
- } from '../core/performance-presets.js';
19
+ } from '../core/index.js';
20
20
  import {
21
21
  measureBundleSizes,
22
22
  toPerformanceData,