@effinrich/forgekit-storybook-plugin 2.0.0
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/README.md +221 -0
- package/bin/forgekit.js +2 -0
- package/dist/chunk-C2HX5UGS.mjs +704 -0
- package/dist/chunk-C2HX5UGS.mjs.map +1 -0
- package/dist/chunk-D2RQPIRR.mjs +413 -0
- package/dist/chunk-D2RQPIRR.mjs.map +1 -0
- package/dist/chunk-T4UFXGMC.js +704 -0
- package/dist/chunk-T4UFXGMC.js.map +1 -0
- package/dist/chunk-WUKJNZOF.js +413 -0
- package/dist/chunk-WUKJNZOF.js.map +1 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +523 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +523 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/forge-test-EGP3AGFI.mjs +7 -0
- package/dist/forge-test-EGP3AGFI.mjs.map +1 -0
- package/dist/forge-test-FLCVDJFR.js +7 -0
- package/dist/forge-test-FLCVDJFR.js.map +1 -0
- package/dist/index.d.mts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +29 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPONENT_EXTENSIONS,
|
|
3
|
+
DEFAULT_DEBOUNCE_MS,
|
|
4
|
+
IGNORED_DIRS,
|
|
5
|
+
STORYBOOK_META_IMPORT,
|
|
6
|
+
STORYBOOK_TEST_IMPORT,
|
|
7
|
+
STORY_FILE_SUFFIX,
|
|
8
|
+
analyzeComponent
|
|
9
|
+
} from "./chunk-D2RQPIRR.mjs";
|
|
10
|
+
|
|
11
|
+
// src/core/scan-directory.ts
|
|
12
|
+
import fg from "fast-glob";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
async function scanDirectory(dir) {
|
|
16
|
+
const absoluteDir = path.resolve(dir);
|
|
17
|
+
if (!fs.existsSync(absoluteDir)) {
|
|
18
|
+
throw new Error(`Directory not found: ${absoluteDir}`);
|
|
19
|
+
}
|
|
20
|
+
const extensions = COMPONENT_EXTENSIONS.map((e) => e.replace(".", "")).join(",");
|
|
21
|
+
const ignorePatterns = [
|
|
22
|
+
...IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
23
|
+
"**/*.spec.*",
|
|
24
|
+
"**/*.test.*",
|
|
25
|
+
"**/*.stories.*",
|
|
26
|
+
"**/*.styles.*",
|
|
27
|
+
"**/*.style.*",
|
|
28
|
+
"**/index.ts",
|
|
29
|
+
"**/index.tsx"
|
|
30
|
+
];
|
|
31
|
+
const files = await fg(`**/*.{${extensions}}`, {
|
|
32
|
+
cwd: absoluteDir,
|
|
33
|
+
ignore: ignorePatterns,
|
|
34
|
+
absolute: true
|
|
35
|
+
});
|
|
36
|
+
const components = [];
|
|
37
|
+
const withStories = [];
|
|
38
|
+
const withoutStories = [];
|
|
39
|
+
const notAnalyzable = [];
|
|
40
|
+
for (const filePath of files) {
|
|
41
|
+
const analysis = analyzeComponent(filePath);
|
|
42
|
+
const storyPath = filePath.replace(/\.tsx?$/, STORY_FILE_SUFFIX);
|
|
43
|
+
const hasStory = fs.existsSync(storyPath);
|
|
44
|
+
components.push({ filePath, analysis, hasStory });
|
|
45
|
+
if (!analysis) {
|
|
46
|
+
notAnalyzable.push(filePath);
|
|
47
|
+
} else if (hasStory) {
|
|
48
|
+
withStories.push(filePath);
|
|
49
|
+
} else {
|
|
50
|
+
withoutStories.push(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
components,
|
|
55
|
+
withStories,
|
|
56
|
+
withoutStories,
|
|
57
|
+
notAnalyzable,
|
|
58
|
+
total: files.length
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/core/generate-interaction-tests.ts
|
|
63
|
+
function generateInteractionTests(analysis) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
const { props, name, hasChildren } = analysis;
|
|
66
|
+
const clickHandlers = props.filter(
|
|
67
|
+
(p) => p.isCallback && (p.name === "onClick" || p.name === "onPress" || p.name === "onSubmit" || p.name === "onClose")
|
|
68
|
+
);
|
|
69
|
+
const toggleProps = props.filter(
|
|
70
|
+
(p) => !p.isCallback && (p.type.toLowerCase() === "boolean" || p.type.toLowerCase() === "bool") && (p.name === "checked" || p.name === "isChecked" || p.name === "selected" || p.name === "open" || p.name === "isOpen")
|
|
71
|
+
);
|
|
72
|
+
if (clickHandlers.length > 0) {
|
|
73
|
+
lines.push(...buildClickTest(name, clickHandlers));
|
|
74
|
+
}
|
|
75
|
+
lines.push(...buildRenderTest(name, hasChildren));
|
|
76
|
+
if (clickHandlers.length > 0 || toggleProps.length > 0) {
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push(...buildKeyboardTest(name, clickHandlers));
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(...buildA11yTest(name, hasChildren));
|
|
82
|
+
return lines;
|
|
83
|
+
}
|
|
84
|
+
function buildClickTest(componentName, clickHandlers) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
const handler = clickHandlers[0];
|
|
87
|
+
lines.push(`export const ClickInteraction: Story = {`);
|
|
88
|
+
lines.push(` args: {`);
|
|
89
|
+
lines.push(` ${handler.name}: fn(),`);
|
|
90
|
+
lines.push(` },`);
|
|
91
|
+
lines.push(` play: async ({ canvasElement, args }) => {`);
|
|
92
|
+
lines.push(` const canvas = within(canvasElement);`);
|
|
93
|
+
lines.push("");
|
|
94
|
+
if (handler.name === "onSubmit") {
|
|
95
|
+
lines.push(
|
|
96
|
+
` const submitButton = canvas.getByRole('button', { name: /submit/i });`
|
|
97
|
+
);
|
|
98
|
+
lines.push(` await userEvent.click(submitButton);`);
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);
|
|
101
|
+
} else if (handler.name === "onClose") {
|
|
102
|
+
lines.push(
|
|
103
|
+
` const closeButton = canvas.getByRole('button', { name: /close/i });`
|
|
104
|
+
);
|
|
105
|
+
lines.push(` await userEvent.click(closeButton);`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(
|
|
110
|
+
` const element = canvas.getByRole('button');`
|
|
111
|
+
);
|
|
112
|
+
lines.push(` await userEvent.click(element);`);
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(` await expect(args.${handler.name}).toHaveBeenCalledTimes(1);`);
|
|
115
|
+
}
|
|
116
|
+
lines.push(` },`);
|
|
117
|
+
lines.push(`};`);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
function buildRenderTest(componentName, hasChildren) {
|
|
121
|
+
const lines = [];
|
|
122
|
+
lines.push("");
|
|
123
|
+
lines.push(`export const RendersCorrectly: Story = {`);
|
|
124
|
+
if (hasChildren) {
|
|
125
|
+
lines.push(` args: {`);
|
|
126
|
+
lines.push(` children: 'Test content',`);
|
|
127
|
+
lines.push(` },`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(` play: async ({ canvasElement }) => {`);
|
|
130
|
+
lines.push(` const canvas = within(canvasElement);`);
|
|
131
|
+
lines.push("");
|
|
132
|
+
if (hasChildren) {
|
|
133
|
+
lines.push(` const element = canvas.getByText('Test content');`);
|
|
134
|
+
lines.push(` await expect(element).toBeInTheDocument();`);
|
|
135
|
+
} else {
|
|
136
|
+
lines.push(` // Verify the component renders without crashing`);
|
|
137
|
+
lines.push(` await expect(canvasElement.firstChild).toBeInTheDocument();`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(` },`);
|
|
140
|
+
lines.push(`};`);
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
function buildA11yTest(componentName, hasChildren) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`export const AccessibilityAudit: Story = {`);
|
|
146
|
+
lines.push(` tags: ['a11y'],`);
|
|
147
|
+
if (hasChildren) {
|
|
148
|
+
lines.push(` args: {`);
|
|
149
|
+
lines.push(` children: '${componentName} content',`);
|
|
150
|
+
lines.push(` },`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(` play: async ({ canvasElement }) => {`);
|
|
153
|
+
lines.push(` const canvas = within(canvasElement);`);
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push(` // Verify component is rendered and visible`);
|
|
156
|
+
if (hasChildren) {
|
|
157
|
+
lines.push(
|
|
158
|
+
` const element = canvas.getByText('${componentName} content');`
|
|
159
|
+
);
|
|
160
|
+
lines.push(` await expect(element).toBeInTheDocument();`);
|
|
161
|
+
} else {
|
|
162
|
+
lines.push(
|
|
163
|
+
` await expect(canvasElement.firstChild).toBeInTheDocument();`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push(` // Verify no implicit ARIA role violations`);
|
|
168
|
+
lines.push(` // The @storybook/addon-a11y will run axe-core checks on this story`);
|
|
169
|
+
lines.push(` // Configure rules in .storybook/preview.tsx via the a11y addon parameter`);
|
|
170
|
+
lines.push(` },`);
|
|
171
|
+
lines.push(`};`);
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
function buildKeyboardTest(_componentName, clickHandlers) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
const handler = clickHandlers[0];
|
|
177
|
+
if (!handler) return lines;
|
|
178
|
+
lines.push(`export const KeyboardNavigation: Story = {`);
|
|
179
|
+
lines.push(` args: {`);
|
|
180
|
+
lines.push(` ${handler.name}: fn(),`);
|
|
181
|
+
lines.push(` },`);
|
|
182
|
+
lines.push(` play: async ({ canvasElement, args }) => {`);
|
|
183
|
+
lines.push(` const canvas = within(canvasElement);`);
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(` const element = canvas.getByRole('button');`);
|
|
186
|
+
lines.push(` await userEvent.tab();`);
|
|
187
|
+
lines.push(` await expect(element).toHaveFocus();`);
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(` await userEvent.keyboard('{Enter}');`);
|
|
190
|
+
lines.push(` await expect(args.${handler.name}).toHaveBeenCalled();`);
|
|
191
|
+
lines.push(` },`);
|
|
192
|
+
lines.push(`};`);
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/core/generate-story-content.ts
|
|
197
|
+
function generateStoryContent(options) {
|
|
198
|
+
const { analysis, storyTitle, importPath, skipInteractionTests } = options;
|
|
199
|
+
const { name } = analysis;
|
|
200
|
+
const lines = [];
|
|
201
|
+
lines.push(...buildImports(analysis, importPath, skipInteractionTests));
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push(...buildMeta(analysis, storyTitle));
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(`type Story = StoryObj<typeof ${name}>;`);
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push(...buildDefaultStory(analysis));
|
|
208
|
+
const variantStories = buildVariantStories(analysis);
|
|
209
|
+
if (variantStories.length > 0) {
|
|
210
|
+
lines.push("");
|
|
211
|
+
lines.push(...variantStories);
|
|
212
|
+
}
|
|
213
|
+
if (!skipInteractionTests) {
|
|
214
|
+
const interactionStories = generateInteractionTests(analysis);
|
|
215
|
+
if (interactionStories.length > 0) {
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push(...interactionStories);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
lines.push("");
|
|
221
|
+
return lines.join("\n");
|
|
222
|
+
}
|
|
223
|
+
function buildImports(analysis, importPath, skipInteractionTests) {
|
|
224
|
+
const lines = [];
|
|
225
|
+
const metaImports = ["Meta", "StoryObj"];
|
|
226
|
+
lines.push(
|
|
227
|
+
`import type { ${metaImports.join(", ")} } from '${STORYBOOK_META_IMPORT}';`
|
|
228
|
+
);
|
|
229
|
+
if (!skipInteractionTests) {
|
|
230
|
+
const testImports = buildTestImports(analysis);
|
|
231
|
+
if (testImports.length > 0) {
|
|
232
|
+
lines.push(
|
|
233
|
+
`import { ${testImports.join(", ")} } from '${STORYBOOK_TEST_IMPORT}';`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (analysis.usesRouter) {
|
|
238
|
+
lines.push(
|
|
239
|
+
`import { withRouter } from 'storybook-addon-react-router-v6';`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
lines.push(`import React from 'react';`);
|
|
243
|
+
if (analysis.exportType === "default") {
|
|
244
|
+
lines.push(`import ${analysis.name} from '${importPath}';`);
|
|
245
|
+
} else {
|
|
246
|
+
lines.push(`import { ${analysis.name} } from '${importPath}';`);
|
|
247
|
+
}
|
|
248
|
+
return lines;
|
|
249
|
+
}
|
|
250
|
+
function buildTestImports(analysis) {
|
|
251
|
+
const imports = /* @__PURE__ */ new Set();
|
|
252
|
+
const { props } = analysis;
|
|
253
|
+
const hasCallbacks = props.some((p) => p.isCallback);
|
|
254
|
+
const hasInteractiveProps = props.some(
|
|
255
|
+
(p) => ["string", "number"].includes(inferControlType(p) ?? "")
|
|
256
|
+
);
|
|
257
|
+
const hasClickable = props.some(
|
|
258
|
+
(p) => p.name === "onClick" || p.name === "onPress"
|
|
259
|
+
);
|
|
260
|
+
if (hasCallbacks) imports.add("fn");
|
|
261
|
+
if (hasClickable || hasInteractiveProps) {
|
|
262
|
+
imports.add("expect");
|
|
263
|
+
imports.add("within");
|
|
264
|
+
}
|
|
265
|
+
if (hasClickable) imports.add("userEvent");
|
|
266
|
+
if (hasInteractiveProps) imports.add("userEvent");
|
|
267
|
+
return Array.from(imports);
|
|
268
|
+
}
|
|
269
|
+
function buildMeta(analysis, storyTitle) {
|
|
270
|
+
const lines = [];
|
|
271
|
+
const { name, props, usesRouter } = analysis;
|
|
272
|
+
lines.push(`const meta: Meta<typeof ${name}> = {`);
|
|
273
|
+
lines.push(` component: ${name},`);
|
|
274
|
+
lines.push(` title: '${storyTitle}',`);
|
|
275
|
+
lines.push(` tags: ['autodocs'],`);
|
|
276
|
+
if (usesRouter) {
|
|
277
|
+
lines.push(` decorators: [withRouter],`);
|
|
278
|
+
}
|
|
279
|
+
const argTypes = buildArgTypes(props);
|
|
280
|
+
if (argTypes.length > 0) {
|
|
281
|
+
lines.push(` argTypes: {`);
|
|
282
|
+
lines.push(...argTypes);
|
|
283
|
+
lines.push(` },`);
|
|
284
|
+
}
|
|
285
|
+
const defaultArgs = buildDefaultArgs(props);
|
|
286
|
+
if (defaultArgs.length > 0) {
|
|
287
|
+
lines.push(` args: {`);
|
|
288
|
+
lines.push(...defaultArgs);
|
|
289
|
+
lines.push(` },`);
|
|
290
|
+
}
|
|
291
|
+
lines.push(`};`);
|
|
292
|
+
lines.push("");
|
|
293
|
+
lines.push(`export default meta;`);
|
|
294
|
+
return lines;
|
|
295
|
+
}
|
|
296
|
+
function buildArgTypes(props) {
|
|
297
|
+
const lines = [];
|
|
298
|
+
for (const prop of props) {
|
|
299
|
+
if (prop.name === "children") continue;
|
|
300
|
+
if (prop.isCallback) {
|
|
301
|
+
lines.push(` ${prop.name}: { action: '${prop.name}' },`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (prop.unionValues && prop.unionValues.length > 0) {
|
|
305
|
+
const options = prop.unionValues.map((v) => `'${v}'`).join(", ");
|
|
306
|
+
lines.push(` ${prop.name}: {`);
|
|
307
|
+
lines.push(` options: [${options}],`);
|
|
308
|
+
lines.push(` control: { type: 'select' },`);
|
|
309
|
+
lines.push(` },`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const controlType = inferControlType(prop);
|
|
313
|
+
if (controlType) {
|
|
314
|
+
lines.push(` ${prop.name}: { control: { type: '${controlType}' } },`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
function buildDefaultArgs(props) {
|
|
320
|
+
const lines = [];
|
|
321
|
+
for (const prop of props) {
|
|
322
|
+
if (!prop.required) continue;
|
|
323
|
+
if (prop.isCallback) {
|
|
324
|
+
lines.push(` ${prop.name}: fn(),`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const defaultValue = inferDefaultValue(prop);
|
|
328
|
+
if (defaultValue !== void 0) {
|
|
329
|
+
lines.push(` ${prop.name}: ${defaultValue},`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return lines;
|
|
333
|
+
}
|
|
334
|
+
function buildDefaultStory(analysis) {
|
|
335
|
+
const lines = [];
|
|
336
|
+
const { hasChildren } = analysis;
|
|
337
|
+
lines.push(`export const Default: Story = {`);
|
|
338
|
+
if (hasChildren) {
|
|
339
|
+
lines.push(` args: {`);
|
|
340
|
+
lines.push(` children: '${analysis.name} content',`);
|
|
341
|
+
lines.push(` },`);
|
|
342
|
+
}
|
|
343
|
+
lines.push(`};`);
|
|
344
|
+
return lines;
|
|
345
|
+
}
|
|
346
|
+
function buildVariantStories(analysis) {
|
|
347
|
+
const lines = [];
|
|
348
|
+
const { props, name } = analysis;
|
|
349
|
+
const sizeProp = props.find((p) => p.name === "size");
|
|
350
|
+
if (sizeProp?.unionValues && sizeProp.unionValues.length > 0) {
|
|
351
|
+
lines.push(`export const Sizes: Story = {`);
|
|
352
|
+
lines.push(` render: (args) => (`);
|
|
353
|
+
lines.push(` <>`);
|
|
354
|
+
for (const size of sizeProp.unionValues) {
|
|
355
|
+
lines.push(
|
|
356
|
+
` <${name} {...args} size="${size}">${analysis.hasChildren ? `Size ${size}` : ""}</${name}>`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
lines.push(` </>`);
|
|
360
|
+
lines.push(` ),`);
|
|
361
|
+
lines.push(`};`);
|
|
362
|
+
lines.push("");
|
|
363
|
+
}
|
|
364
|
+
const variantProp = props.find((p) => p.name === "variant");
|
|
365
|
+
if (variantProp?.unionValues && variantProp.unionValues.length > 0) {
|
|
366
|
+
lines.push(`export const Variants: Story = {`);
|
|
367
|
+
lines.push(` render: (args) => (`);
|
|
368
|
+
lines.push(` <>`);
|
|
369
|
+
for (const variant of variantProp.unionValues) {
|
|
370
|
+
lines.push(
|
|
371
|
+
` <${name} {...args} variant="${variant}">${analysis.hasChildren ? `Variant ${variant}` : ""}</${name}>`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
lines.push(` </>`);
|
|
375
|
+
lines.push(` ),`);
|
|
376
|
+
lines.push(`};`);
|
|
377
|
+
lines.push("");
|
|
378
|
+
}
|
|
379
|
+
const colorPaletteProp = props.find((p) => p.name === "colorPalette");
|
|
380
|
+
if (colorPaletteProp?.unionValues && colorPaletteProp.unionValues.length > 0) {
|
|
381
|
+
lines.push(`export const ColorPalettes: Story = {`);
|
|
382
|
+
lines.push(` render: (args) => (`);
|
|
383
|
+
lines.push(` <>`);
|
|
384
|
+
for (const palette of colorPaletteProp.unionValues) {
|
|
385
|
+
lines.push(
|
|
386
|
+
` <${name} {...args} colorPalette="${palette}">${analysis.hasChildren ? palette : ""}</${name}>`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
lines.push(` </>`);
|
|
390
|
+
lines.push(` ),`);
|
|
391
|
+
lines.push(`};`);
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
const disabledProp = props.find(
|
|
395
|
+
(p) => p.name === "disabled" || p.name === "isDisabled"
|
|
396
|
+
);
|
|
397
|
+
if (disabledProp) {
|
|
398
|
+
lines.push(`export const Disabled: Story = {`);
|
|
399
|
+
lines.push(` args: {`);
|
|
400
|
+
lines.push(` ${disabledProp.name}: true,`);
|
|
401
|
+
lines.push(` },`);
|
|
402
|
+
lines.push(`};`);
|
|
403
|
+
}
|
|
404
|
+
return lines;
|
|
405
|
+
}
|
|
406
|
+
function inferControlType(prop) {
|
|
407
|
+
const type = prop.type.toLowerCase();
|
|
408
|
+
if (type === "boolean" || type === "bool") return "boolean";
|
|
409
|
+
if (type === "string") return "text";
|
|
410
|
+
if (type === "number") return "number";
|
|
411
|
+
if (type.includes("react.reactnode") || type.includes("reactnode"))
|
|
412
|
+
return null;
|
|
413
|
+
if (prop.isCallback) return null;
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function inferDefaultValue(prop) {
|
|
417
|
+
if (prop.defaultValue) return prop.defaultValue;
|
|
418
|
+
const type = prop.type.toLowerCase();
|
|
419
|
+
if (type === "string") return `'Example ${prop.name}'`;
|
|
420
|
+
if (type === "number") return "0";
|
|
421
|
+
if (type === "boolean" || type === "bool") return "false";
|
|
422
|
+
if (prop.unionValues && prop.unionValues.length > 0) {
|
|
423
|
+
return `'${prop.unionValues[0]}'`;
|
|
424
|
+
}
|
|
425
|
+
if (prop.isCallback) return "fn()";
|
|
426
|
+
return void 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/core/score-coverage.ts
|
|
430
|
+
function scoreCoverage(covered, total) {
|
|
431
|
+
if (total === 0) {
|
|
432
|
+
return { covered: 0, total: 0, percentage: 0, grade: "F" };
|
|
433
|
+
}
|
|
434
|
+
const percentage = Math.round(covered / total * 100);
|
|
435
|
+
let grade;
|
|
436
|
+
if (percentage >= 90) grade = "A";
|
|
437
|
+
else if (percentage >= 75) grade = "B";
|
|
438
|
+
else if (percentage >= 50) grade = "C";
|
|
439
|
+
else if (percentage >= 25) grade = "D";
|
|
440
|
+
else grade = "F";
|
|
441
|
+
return { covered, total, percentage, grade };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/core/infer-title.ts
|
|
445
|
+
import * as path2 from "path";
|
|
446
|
+
function inferStoryTitle(filePath, baseDir) {
|
|
447
|
+
const resolvedBase = baseDir ? path2.resolve(baseDir) : path2.dirname(filePath);
|
|
448
|
+
const resolvedFile = path2.resolve(filePath);
|
|
449
|
+
let relative2 = path2.relative(resolvedBase, resolvedFile);
|
|
450
|
+
const fileName = path2.basename(relative2, path2.extname(relative2));
|
|
451
|
+
const dirPart = path2.dirname(relative2);
|
|
452
|
+
if (dirPart === ".") {
|
|
453
|
+
return `Components / ${toPascalCase(fileName)}`;
|
|
454
|
+
}
|
|
455
|
+
let segments = dirPart.split(path2.sep);
|
|
456
|
+
const stripPrefixes = ["src", "lib", "source"];
|
|
457
|
+
while (segments.length > 0 && stripPrefixes.includes(segments[0].toLowerCase())) {
|
|
458
|
+
segments.shift();
|
|
459
|
+
}
|
|
460
|
+
const parts = segments.map((seg) => toPascalCase(seg));
|
|
461
|
+
const componentName = toPascalCase(fileName);
|
|
462
|
+
parts.push(componentName);
|
|
463
|
+
const deduped = [];
|
|
464
|
+
for (const part of parts) {
|
|
465
|
+
if (part !== deduped[deduped.length - 1]) {
|
|
466
|
+
deduped.push(part);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (deduped.length === 0) {
|
|
470
|
+
return `Components / ${componentName}`;
|
|
471
|
+
}
|
|
472
|
+
return deduped.join(" / ");
|
|
473
|
+
}
|
|
474
|
+
function toPascalCase(str) {
|
|
475
|
+
return str.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/api/forge-story.ts
|
|
479
|
+
import * as fs2 from "fs";
|
|
480
|
+
import * as path3 from "path";
|
|
481
|
+
async function forgeStory(options) {
|
|
482
|
+
const { componentPath, storyTitle, skipInteractionTests = false, overwrite = false, dryRun = false } = options;
|
|
483
|
+
const resolvedPath = resolveComponentPath(componentPath);
|
|
484
|
+
if (!resolvedPath) {
|
|
485
|
+
throw new Error(`Component file not found: ${componentPath}`);
|
|
486
|
+
}
|
|
487
|
+
const storyPath = resolvedPath.replace(/\.tsx?$/, STORY_FILE_SUFFIX);
|
|
488
|
+
if (fs2.existsSync(storyPath) && !overwrite) {
|
|
489
|
+
throw new Error(`Story already exists: ${storyPath}. Use --overwrite to replace.`);
|
|
490
|
+
}
|
|
491
|
+
const analysis = analyzeComponent(resolvedPath);
|
|
492
|
+
if (!analysis) {
|
|
493
|
+
throw new Error(`Could not analyze component at ${resolvedPath}. Ensure it exports a React component.`);
|
|
494
|
+
}
|
|
495
|
+
const title = storyTitle ?? inferStoryTitle(resolvedPath);
|
|
496
|
+
const importPath = `./${analysis.fileName}`;
|
|
497
|
+
const content = generateStoryContent({
|
|
498
|
+
analysis,
|
|
499
|
+
storyTitle: title,
|
|
500
|
+
importPath,
|
|
501
|
+
skipInteractionTests
|
|
502
|
+
});
|
|
503
|
+
const storiesGenerated = collectStoriesGenerated(analysis, skipInteractionTests);
|
|
504
|
+
let written = false;
|
|
505
|
+
if (!dryRun) {
|
|
506
|
+
const dir = path3.dirname(storyPath);
|
|
507
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
508
|
+
fs2.writeFileSync(storyPath, content, "utf-8");
|
|
509
|
+
written = true;
|
|
510
|
+
}
|
|
511
|
+
return { storyPath, content, analysis, storiesGenerated, written };
|
|
512
|
+
}
|
|
513
|
+
function resolveComponentPath(componentPath) {
|
|
514
|
+
const resolved = path3.resolve(componentPath);
|
|
515
|
+
if (fs2.existsSync(resolved)) return resolved;
|
|
516
|
+
for (const ext of COMPONENT_EXTENSIONS) {
|
|
517
|
+
const withExt = resolved + ext;
|
|
518
|
+
if (fs2.existsSync(withExt)) return withExt;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
function collectStoriesGenerated(analysis, skipInteractionTests) {
|
|
523
|
+
const stories = ["Default"];
|
|
524
|
+
if (analysis.props.some((p) => p.name === "size" && p.unionValues?.length)) {
|
|
525
|
+
stories.push("Sizes");
|
|
526
|
+
}
|
|
527
|
+
if (analysis.props.some((p) => p.name === "variant" && p.unionValues?.length)) {
|
|
528
|
+
stories.push("Variants");
|
|
529
|
+
}
|
|
530
|
+
if (analysis.props.some((p) => p.name === "colorPalette" && p.unionValues?.length)) {
|
|
531
|
+
stories.push("ColorPalettes");
|
|
532
|
+
}
|
|
533
|
+
if (analysis.props.some((p) => p.name === "disabled" || p.name === "isDisabled")) {
|
|
534
|
+
stories.push("Disabled");
|
|
535
|
+
}
|
|
536
|
+
if (!skipInteractionTests) {
|
|
537
|
+
stories.push("RendersCorrectly", "AccessibilityAudit");
|
|
538
|
+
if (analysis.props.some(
|
|
539
|
+
(p) => p.isCallback && ["onClick", "onPress", "onSubmit", "onClose"].includes(p.name)
|
|
540
|
+
)) {
|
|
541
|
+
stories.push("ClickInteraction", "KeyboardNavigation");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return stories;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/core/watch.ts
|
|
548
|
+
import { watch as chokidarWatch } from "chokidar";
|
|
549
|
+
import * as path4 from "path";
|
|
550
|
+
import * as fs3 from "fs";
|
|
551
|
+
function watchDirectory(options, callback) {
|
|
552
|
+
const {
|
|
553
|
+
dir,
|
|
554
|
+
ignore = [],
|
|
555
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
556
|
+
skipInteractionTests = false
|
|
557
|
+
} = options;
|
|
558
|
+
const absoluteDir = path4.resolve(dir);
|
|
559
|
+
if (!fs3.existsSync(absoluteDir)) {
|
|
560
|
+
throw new Error(`Watch directory not found: ${absoluteDir}`);
|
|
561
|
+
}
|
|
562
|
+
const extensions = COMPONENT_EXTENSIONS.map((e) => e.replace(".", ""));
|
|
563
|
+
const globPattern = `**/*.{${extensions.join(",")}}`;
|
|
564
|
+
const ignored = [
|
|
565
|
+
...IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
566
|
+
"**/*.spec.*",
|
|
567
|
+
"**/*.test.*",
|
|
568
|
+
"**/*.stories.*",
|
|
569
|
+
"**/*.styles.*",
|
|
570
|
+
"**/*.style.*",
|
|
571
|
+
"**/index.{ts,tsx}",
|
|
572
|
+
...ignore
|
|
573
|
+
];
|
|
574
|
+
const pending = /* @__PURE__ */ new Map();
|
|
575
|
+
const watcher = chokidarWatch(globPattern, {
|
|
576
|
+
cwd: absoluteDir,
|
|
577
|
+
ignored,
|
|
578
|
+
ignoreInitial: true,
|
|
579
|
+
awaitWriteFinish: {
|
|
580
|
+
stabilityThreshold: 100,
|
|
581
|
+
pollInterval: 50
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
const processChange = async (relativePath) => {
|
|
585
|
+
const filePath = path4.join(absoluteDir, relativePath);
|
|
586
|
+
const storyPath = filePath.replace(/\.tsx?$/, STORY_FILE_SUFFIX);
|
|
587
|
+
const isUpdate = fs3.existsSync(storyPath);
|
|
588
|
+
try {
|
|
589
|
+
await forgeStory({
|
|
590
|
+
componentPath: filePath,
|
|
591
|
+
skipInteractionTests,
|
|
592
|
+
overwrite: true,
|
|
593
|
+
quiet: true
|
|
594
|
+
});
|
|
595
|
+
callback?.({
|
|
596
|
+
type: isUpdate ? "update" : "generate",
|
|
597
|
+
file: filePath,
|
|
598
|
+
storyPath
|
|
599
|
+
});
|
|
600
|
+
} catch (err) {
|
|
601
|
+
callback?.({
|
|
602
|
+
type: "error",
|
|
603
|
+
file: filePath,
|
|
604
|
+
error: err
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
const debouncedProcess = (relativePath) => {
|
|
609
|
+
if (pending.has(relativePath)) {
|
|
610
|
+
clearTimeout(pending.get(relativePath));
|
|
611
|
+
}
|
|
612
|
+
pending.set(
|
|
613
|
+
relativePath,
|
|
614
|
+
setTimeout(() => {
|
|
615
|
+
pending.delete(relativePath);
|
|
616
|
+
processChange(relativePath);
|
|
617
|
+
}, debounceMs)
|
|
618
|
+
);
|
|
619
|
+
};
|
|
620
|
+
watcher.on("change", debouncedProcess);
|
|
621
|
+
watcher.on("add", debouncedProcess);
|
|
622
|
+
watcher.on("ready", () => callback?.({ type: "ready" }));
|
|
623
|
+
return {
|
|
624
|
+
async close() {
|
|
625
|
+
for (const timeout of pending.values()) {
|
|
626
|
+
clearTimeout(timeout);
|
|
627
|
+
}
|
|
628
|
+
pending.clear();
|
|
629
|
+
await watcher.close();
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/api/forge-stories.ts
|
|
635
|
+
import * as path5 from "path";
|
|
636
|
+
async function forgeStories(options) {
|
|
637
|
+
const {
|
|
638
|
+
dir,
|
|
639
|
+
skipInteractionTests = false,
|
|
640
|
+
overwrite = false,
|
|
641
|
+
dryRun = false,
|
|
642
|
+
includeComponentTests = false
|
|
643
|
+
} = options;
|
|
644
|
+
const { forgeTest } = includeComponentTests ? await import("./forge-test-EGP3AGFI.mjs") : { forgeTest: null };
|
|
645
|
+
const absoluteDir = path5.resolve(dir);
|
|
646
|
+
const scan = await scanDirectory(absoluteDir);
|
|
647
|
+
let generated = 0;
|
|
648
|
+
let failed = 0;
|
|
649
|
+
const errors = [];
|
|
650
|
+
const filesToProcess = overwrite ? [...scan.withoutStories, ...scan.withStories] : scan.withoutStories;
|
|
651
|
+
for (const filePath of filesToProcess) {
|
|
652
|
+
try {
|
|
653
|
+
await forgeStory({
|
|
654
|
+
componentPath: filePath,
|
|
655
|
+
skipInteractionTests,
|
|
656
|
+
overwrite: true,
|
|
657
|
+
dryRun,
|
|
658
|
+
quiet: true
|
|
659
|
+
});
|
|
660
|
+
generated++;
|
|
661
|
+
if (forgeTest) {
|
|
662
|
+
try {
|
|
663
|
+
await forgeTest({
|
|
664
|
+
componentPath: filePath,
|
|
665
|
+
overwrite: true,
|
|
666
|
+
dryRun,
|
|
667
|
+
quiet: true
|
|
668
|
+
});
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (err) {
|
|
673
|
+
failed++;
|
|
674
|
+
errors.push({
|
|
675
|
+
file: filePath,
|
|
676
|
+
error: err.message
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const totalAnalyzable = scan.total - scan.notAnalyzable.length;
|
|
681
|
+
const totalCovered = overwrite ? generated : scan.withStories.length + generated;
|
|
682
|
+
const coverage = scoreCoverage(totalCovered, totalAnalyzable);
|
|
683
|
+
return {
|
|
684
|
+
generated,
|
|
685
|
+
failed,
|
|
686
|
+
alreadyCovered: scan.withStories.length,
|
|
687
|
+
notAnalyzable: scan.notAnalyzable.length,
|
|
688
|
+
total: totalAnalyzable,
|
|
689
|
+
coverage,
|
|
690
|
+
errors
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export {
|
|
695
|
+
scanDirectory,
|
|
696
|
+
generateInteractionTests,
|
|
697
|
+
generateStoryContent,
|
|
698
|
+
scoreCoverage,
|
|
699
|
+
inferStoryTitle,
|
|
700
|
+
forgeStory,
|
|
701
|
+
watchDirectory,
|
|
702
|
+
forgeStories
|
|
703
|
+
};
|
|
704
|
+
//# sourceMappingURL=chunk-C2HX5UGS.mjs.map
|