@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.
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +435 -67
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-D7372LQX.js → chunk-5G3VZH43.js} +8 -12
- package/dist/chunk-5G3VZH43.js.map +1 -0
- package/dist/chunk-D2CDBRNU.js +2 -0
- package/dist/{chunk-YMPGYEWK.js → chunk-D5PYOXEI.js} +2 -2
- package/dist/{chunk-BW3ZATBW.js → chunk-HRFUSSZI.js} +27 -10
- package/dist/chunk-HRFUSSZI.js.map +1 -0
- package/dist/{chunk-GF6OVPIN.js → chunk-OQO55NKV.js} +405 -34
- package/dist/chunk-OQO55NKV.js.map +1 -0
- package/dist/{chunk-TOIE7VXF.js → chunk-PW7QTQA6.js} +2 -2
- package/dist/{chunk-AWYCDRPG.js → chunk-WXSR2II7.js} +2 -2
- package/dist/chunk-WXSR2II7.js.map +1 -0
- package/dist/{chunk-5GT62FCB.js → chunk-ZM4ZQZWZ.js} +5 -5
- package/dist/core/index.d.ts +1 -2194
- package/dist/core/index.js +22 -27
- package/dist/{discovery-Z4RDDFVR.js → discovery-NEOY4MPN.js} +3 -3
- package/dist/{generate-LQA2R7FN.js → generate-FBHSXR3D.js} +5 -7
- package/dist/{generate-LQA2R7FN.js.map → generate-FBHSXR3D.js.map} +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +7 -9
- package/dist/index.js.map +1 -1
- package/dist/{init-2GEGVIUQ.js → init-NDQXUWDU.js} +58 -6
- package/dist/init-NDQXUWDU.js.map +1 -0
- package/dist/mcp-bin.js +5 -8
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-CJF2DOQW.js +14 -0
- package/dist/scan-generate-SJAN5MVI.js +691 -0
- package/dist/scan-generate-SJAN5MVI.js.map +1 -0
- package/dist/{service-XP2EAJXD.js → service-TQYWY65E.js} +4 -6
- package/dist/{static-viewer-XCS7UJTO.js → static-viewer-NUBFPKWH.js} +4 -6
- package/dist/{test-TD6TJNVY.js → test-Z5LVO724.js} +4 -5
- package/dist/{test-TD6TJNVY.js.map → test-Z5LVO724.js.map} +1 -1
- package/dist/{tokens-2EXPCVP3.js → tokens-CE46OTMD.js} +6 -8
- package/dist/{tokens-2EXPCVP3.js.map → tokens-CE46OTMD.js.map} +1 -1
- package/dist/{viewer-RFA2KVBG.js → viewer-DNMNC5VS.js} +16 -19
- package/dist/viewer-DNMNC5VS.js.map +1 -0
- package/package.json +2 -1
- package/src/bin.ts +33 -1
- package/src/build.ts +1 -1
- package/src/commands/__tests__/scan-generate.test.ts +308 -0
- package/src/commands/init.ts +72 -5
- package/src/commands/perf.ts +1 -1
- package/src/commands/scan-generate.ts +1013 -0
- package/src/commands/setup.ts +499 -0
- package/src/core/auto-props.ts +1 -1
- package/src/core/bundle-measurer.ts +2 -2
- package/src/core/config.ts +2 -3
- package/src/core/discovery.ts +2 -2
- package/src/core/generators/context.ts +1 -1
- package/src/core/generators/registry.ts +3 -3
- package/src/core/generators/typescript-extractor.ts +1 -1
- package/src/core/graph-extractor.ts +1 -1
- package/src/core/index.ts +3 -205
- package/src/core/loader.ts +40 -10
- package/src/core/parser.ts +1 -1
- package/src/core/previewLoader.ts +1 -1
- package/src/index.ts +2 -2
- package/src/service/snippet-validation.test.ts +1 -1
- package/src/service/snippet-validation.ts +2 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +3 -9
- package/src/viewer/assets/fragments_logo.png +0 -0
- package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +2 -10
- package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +4 -1
- package/src/viewer/vite-plugin.ts +1 -1
- package/dist/chunk-AWYCDRPG.js.map +0 -1
- package/dist/chunk-BW3ZATBW.js.map +0 -1
- package/dist/chunk-D7372LQX.js.map +0 -1
- package/dist/chunk-EKLMXTWU.js +0 -80
- package/dist/chunk-EKLMXTWU.js.map +0 -1
- package/dist/chunk-EZYXYWNF.js +0 -131
- package/dist/chunk-EZYXYWNF.js.map +0 -1
- package/dist/chunk-GF6OVPIN.js.map +0 -1
- package/dist/chunk-NVSPGSKB.js +0 -203
- package/dist/chunk-NVSPGSKB.js.map +0 -1
- package/dist/defineFragment-CBMS7Bab.d.ts +0 -685
- package/dist/init-2GEGVIUQ.js.map +0 -1
- package/dist/scan-JGS65S7P.js +0 -16
- package/dist/storyFilters-3LUYAFZF.js +0 -15
- package/dist/viewer-RFA2KVBG.js.map +0 -1
- package/src/core/__tests__/preview-runtime.test.tsx +0 -111
- package/src/core/composition.test.ts +0 -262
- package/src/core/composition.ts +0 -318
- package/src/core/constants.ts +0 -114
- package/src/core/context.ts +0 -2
- package/src/core/defineFragment.ts +0 -141
- package/src/core/figma.ts +0 -263
- package/src/core/fragment-types.ts +0 -214
- package/src/core/performance-presets.ts +0 -142
- package/src/core/preview-runtime.tsx +0 -144
- package/src/core/schema.ts +0 -229
- package/src/core/storyAdapter.test.ts +0 -571
- package/src/core/storyAdapter.ts +0 -761
- package/src/core/storyFilters.test.ts +0 -350
- package/src/core/storyFilters.ts +0 -253
- package/src/core/storybook-csf.ts +0 -11
- package/src/core/token-parser.ts +0 -321
- package/src/core/token-types.ts +0 -287
- package/src/core/types.ts +0 -784
- /package/dist/{discovery-Z4RDDFVR.js.map → chunk-D2CDBRNU.js.map} +0 -0
- /package/dist/{chunk-YMPGYEWK.js.map → chunk-D5PYOXEI.js.map} +0 -0
- /package/dist/{chunk-TOIE7VXF.js.map → chunk-PW7QTQA6.js.map} +0 -0
- /package/dist/{chunk-5GT62FCB.js.map → chunk-ZM4ZQZWZ.js.map} +0 -0
- /package/dist/{scan-JGS65S7P.js.map → discovery-NEOY4MPN.js.map} +0 -0
- /package/dist/{service-XP2EAJXD.js.map → scan-CJF2DOQW.js.map} +0 -0
- /package/dist/{static-viewer-XCS7UJTO.js.map → service-TQYWY65E.js.map} +0 -0
- /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
|
+
});
|
package/src/commands/init.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fragments init - Smart interactive initialization
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
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
|