@onlook/storybook-plugin 0.4.0-beta.5 → 0.4.0-beta.6
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/index.js +267 -294
- package/package.json +1 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs2, { existsSync } from 'fs';
|
|
2
|
+
import path4, { dirname, join, relative } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { minimatch } from 'minimatch';
|
|
5
|
-
import
|
|
6
|
-
import { Project, SyntaxKind, ts, TypeFlags } from 'ts-morph';
|
|
5
|
+
import { Project, ts, TypeFlags } from 'ts-morph';
|
|
7
6
|
import { glob } from 'glob';
|
|
8
7
|
import { withDefaultConfig } from 'react-docgen-typescript';
|
|
9
8
|
import generateModule from '@babel/generator';
|
|
@@ -15,7 +14,7 @@ import { chromium } from 'playwright';
|
|
|
15
14
|
|
|
16
15
|
// src/storybook-onlook-plugin.ts
|
|
17
16
|
function getComponentInfo(componentDir, projectRootDir) {
|
|
18
|
-
const fileParseInfo =
|
|
17
|
+
const fileParseInfo = path4.parse(componentDir);
|
|
19
18
|
const prefixExtRegex = new RegExp(`(\\.\\w+)+(?=\\${fileParseInfo.ext}$)`);
|
|
20
19
|
const prefixExtRegexMatch = fileParseInfo.base.match(prefixExtRegex);
|
|
21
20
|
const prefixExt = prefixExtRegexMatch ? prefixExtRegexMatch[0] : void 0;
|
|
@@ -187,55 +186,116 @@ var throwErr = (params) => {
|
|
|
187
186
|
console.error(`[ASG:${errorCode}] ${errorDefinition[errorCode].title}
|
|
188
187
|
${detailText}`);
|
|
189
188
|
};
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
storiesFolder
|
|
201
|
-
}) {
|
|
189
|
+
function genReactStoryFile(input) {
|
|
190
|
+
const {
|
|
191
|
+
componentName,
|
|
192
|
+
fileBase,
|
|
193
|
+
fileName,
|
|
194
|
+
filePrefixExt,
|
|
195
|
+
relativeSourceFilePath,
|
|
196
|
+
sourceFile,
|
|
197
|
+
storiesFolder
|
|
198
|
+
} = input;
|
|
202
199
|
if (!componentName || !fileBase) {
|
|
203
|
-
throwErr({
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
return;
|
|
200
|
+
throwErr({ errorCode: "EC03" });
|
|
201
|
+
return null;
|
|
207
202
|
}
|
|
208
|
-
const { propTypes } = getReactPropTypes({
|
|
209
|
-
sourceFile,
|
|
210
|
-
componentName
|
|
211
|
-
});
|
|
203
|
+
const { propTypes } = getReactPropTypes({ sourceFile, componentName });
|
|
212
204
|
const pascalComponentName = pascalCase(componentName);
|
|
213
205
|
if (!propTypes) {
|
|
214
|
-
throwErr({
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
return;
|
|
206
|
+
throwErr({ errorCode: "EC04" });
|
|
207
|
+
return null;
|
|
218
208
|
}
|
|
219
|
-
const
|
|
209
|
+
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
220
210
|
let isDefaultExportComponent = false;
|
|
221
|
-
|
|
211
|
+
exportedDeclarations.forEach((declaration, exportName) => {
|
|
222
212
|
if (exportName === "default") {
|
|
223
213
|
const defaultExportName = declaration[0]?.getSymbol()?.getName();
|
|
224
214
|
isDefaultExportComponent = defaultExportName === pascalComponentName;
|
|
225
215
|
}
|
|
226
216
|
});
|
|
227
217
|
const pathToComponent = storiesFolder ? "../" : "./";
|
|
228
|
-
const
|
|
218
|
+
const importSuffix = filePrefixExt || "";
|
|
219
|
+
const importStatement = isDefaultExportComponent ? `import ${pascalComponentName} from "${pathToComponent}${fileName}${importSuffix}";` : `import { ${pascalComponentName} } from "${pathToComponent}${fileName}${importSuffix}";`;
|
|
220
|
+
const args = {};
|
|
221
|
+
for (const prop of propTypes) {
|
|
222
|
+
if (prop.isOptional) {
|
|
223
|
+
args[prop.name] = "undefined";
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (prop.type.includes("boolean")) {
|
|
227
|
+
args[prop.name] = "false";
|
|
228
|
+
} else if (prop.value.length > 0) {
|
|
229
|
+
args[prop.name] = `"${prop.value[0]}"`;
|
|
230
|
+
} else if (prop.type.includes("string") || prop.type.includes("String")) {
|
|
231
|
+
args[prop.name] = `"${prop.name}"`;
|
|
232
|
+
} else if (prop.type.includes("number") || prop.type.includes("Number")) {
|
|
233
|
+
args[prop.name] = "0";
|
|
234
|
+
} else {
|
|
235
|
+
args[prop.name] = "undefined";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const argTypes = {};
|
|
239
|
+
for (const prop of propTypes) {
|
|
240
|
+
if (prop.type[0] === "boolean") {
|
|
241
|
+
argTypes[prop.name] = '{ control: "boolean" }';
|
|
242
|
+
} else if (prop.type[0] === "object") {
|
|
243
|
+
argTypes[prop.name] = '{ control: "object" }';
|
|
244
|
+
} else if (prop.value.length > 1) {
|
|
245
|
+
const options = prop.value.map((v) => `"${v}"`).join(", ");
|
|
246
|
+
argTypes[prop.name] = `{ control: "select", options: [${options}] }`;
|
|
247
|
+
} else if (prop.type[0] === "string") {
|
|
248
|
+
argTypes[prop.name] = '{ control: "text" }';
|
|
249
|
+
} else if (prop.type[0] === "number") {
|
|
250
|
+
argTypes[prop.name] = '{ control: "number" }';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const argsStr = formatObjectEntries(args, " ");
|
|
254
|
+
const argTypesStr = formatObjectEntries(argTypes, " ");
|
|
255
|
+
const storyTitle = buildStoryTitle(relativeSourceFilePath, pascalComponentName);
|
|
256
|
+
return `import * as _React from "react";
|
|
229
257
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
258
|
+
${importStatement}
|
|
230
259
|
|
|
231
|
-
|
|
260
|
+
// Inline error boundary \u2014 catches render crashes from missing providers/context
|
|
261
|
+
class _StoryErrorBoundary extends _React.Component<
|
|
262
|
+
{ children: _React.ReactNode },
|
|
263
|
+
{ error: Error | null }
|
|
264
|
+
> {
|
|
265
|
+
state: { error: Error | null } = { error: null };
|
|
266
|
+
static getDerivedStateFromError(error: Error) { return { error }; }
|
|
267
|
+
render() {
|
|
268
|
+
if (this.state.error) {
|
|
269
|
+
return (
|
|
270
|
+
<div style={{ padding: 24, fontFamily: "system-ui", color: "#888", fontSize: 13 }}>
|
|
271
|
+
<div style={{ fontWeight: 600, marginBottom: 8, color: "#c44" }}>
|
|
272
|
+
Cannot render in isolation
|
|
273
|
+
</div>
|
|
274
|
+
<div style={{ fontSize: 12, opacity: 0.7 }}>
|
|
275
|
+
{this.state.error.message}
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return this.props.children;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
232
283
|
|
|
233
284
|
const meta: Meta<typeof ${pascalComponentName}> = {
|
|
234
|
-
title: "
|
|
235
|
-
component:
|
|
285
|
+
title: "${storyTitle}",
|
|
286
|
+
component: ${pascalComponentName},
|
|
236
287
|
tags: ["autodocs"],
|
|
237
|
-
args: {},
|
|
238
|
-
argTypes: {},
|
|
288
|
+
args: {${argsStr}},
|
|
289
|
+
argTypes: {${argTypesStr}},
|
|
290
|
+
decorators: [
|
|
291
|
+
(Story) => (
|
|
292
|
+
<_StoryErrorBoundary>
|
|
293
|
+
<_React.Suspense fallback={<div style={{ padding: 24, color: "#888" }}>Loading...</div>}>
|
|
294
|
+
<Story />
|
|
295
|
+
</_React.Suspense>
|
|
296
|
+
</_StoryErrorBoundary>
|
|
297
|
+
),
|
|
298
|
+
],
|
|
239
299
|
};
|
|
240
300
|
|
|
241
301
|
export default meta;
|
|
@@ -243,66 +303,26 @@ type Story = StoryObj<typeof meta>;
|
|
|
243
303
|
|
|
244
304
|
export const Primary: Story = {};
|
|
245
305
|
`;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (prop.value.length > 1) {
|
|
267
|
-
return argTypes[prop.name] = {
|
|
268
|
-
control: "select",
|
|
269
|
-
options: prop.value
|
|
270
|
-
};
|
|
271
|
-
} else {
|
|
272
|
-
if (prop.type[0] === "string") {
|
|
273
|
-
return argTypes[prop.name] = {
|
|
274
|
-
control: "text"
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
if (prop.type[0] === "number") {
|
|
278
|
-
return argTypes[prop.name] = {
|
|
279
|
-
control: "number"
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
return {
|
|
285
|
-
fileOptions: {
|
|
286
|
-
componentName,
|
|
287
|
-
fileBase,
|
|
288
|
-
fileName,
|
|
289
|
-
filePrefixExt,
|
|
290
|
-
path: path42,
|
|
291
|
-
fileExt,
|
|
292
|
-
relativeSourceFilePath,
|
|
293
|
-
sourceFile,
|
|
294
|
-
prettierConfigPath
|
|
295
|
-
},
|
|
296
|
-
generateOptions: {
|
|
297
|
-
fileExt: ".stories.tsx",
|
|
298
|
-
initialCode,
|
|
299
|
-
meta: {
|
|
300
|
-
component: componentCode,
|
|
301
|
-
args,
|
|
302
|
-
argTypes
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
+
}
|
|
307
|
+
function buildStoryTitle(relPath, componentName) {
|
|
308
|
+
let p = relPath.replace(/^src\//, "");
|
|
309
|
+
const lastSlash = p.lastIndexOf("/");
|
|
310
|
+
if (lastSlash !== -1) {
|
|
311
|
+
p = p.substring(0, lastSlash);
|
|
312
|
+
} else {
|
|
313
|
+
p = "";
|
|
314
|
+
}
|
|
315
|
+
p = p.replace(/\(([^)]+)\)/g, "$1");
|
|
316
|
+
p = p.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
317
|
+
return p ? `${p}/${componentName}` : componentName;
|
|
318
|
+
}
|
|
319
|
+
function formatObjectEntries(obj, indent) {
|
|
320
|
+
const entries = Object.entries(obj);
|
|
321
|
+
if (entries.length === 0) return "";
|
|
322
|
+
const lines = entries.map(([key, value]) => `${indent}${key}: ${value},`);
|
|
323
|
+
return `
|
|
324
|
+
${lines.join("\n")}
|
|
325
|
+
`;
|
|
306
326
|
}
|
|
307
327
|
function createLightProject() {
|
|
308
328
|
return new Project({
|
|
@@ -310,25 +330,12 @@ function createLightProject() {
|
|
|
310
330
|
useInMemoryFileSystem: true
|
|
311
331
|
});
|
|
312
332
|
}
|
|
313
|
-
var resolvedPrettierConfig = null;
|
|
314
|
-
async function getPrettierConfig(prettierConfigPath) {
|
|
315
|
-
if (resolvedPrettierConfig) return resolvedPrettierConfig;
|
|
316
|
-
resolvedPrettierConfig = prettierConfigPath ? await prettier.resolveConfig(prettierConfigPath) : {
|
|
317
|
-
semi: true,
|
|
318
|
-
trailingComma: "all",
|
|
319
|
-
singleQuote: false,
|
|
320
|
-
printWidth: 80,
|
|
321
|
-
tabWidth: 2,
|
|
322
|
-
endOfLine: "lf"
|
|
323
|
-
};
|
|
324
|
-
return resolvedPrettierConfig ?? {};
|
|
325
|
-
}
|
|
326
333
|
async function genStoryFile({
|
|
327
334
|
options,
|
|
328
335
|
id,
|
|
329
336
|
projectRootDir
|
|
330
337
|
}) {
|
|
331
|
-
if (id.includes(".stories")) return;
|
|
338
|
+
if (id.includes(".stories")) return null;
|
|
332
339
|
const {
|
|
333
340
|
fileBase,
|
|
334
341
|
fileName,
|
|
@@ -338,145 +345,45 @@ async function genStoryFile({
|
|
|
338
345
|
relativeSourceFilePath
|
|
339
346
|
} = getComponentInfo(id, projectRootDir);
|
|
340
347
|
try {
|
|
341
|
-
const sourceCode =
|
|
348
|
+
const sourceCode = fs2.readFileSync(id, "utf-8");
|
|
342
349
|
const sourceProject = createLightProject();
|
|
343
350
|
const sourceFile = sourceProject.createSourceFile(fileBase || "temp.tsx", sourceCode);
|
|
344
|
-
|
|
345
|
-
if (options.preset === "react") {
|
|
346
|
-
genStoryFileOptions = await genReactStoryFile({
|
|
347
|
-
componentName,
|
|
348
|
-
fileBase,
|
|
349
|
-
fileName,
|
|
350
|
-
path: id,
|
|
351
|
-
fileExt,
|
|
352
|
-
filePrefixExt,
|
|
353
|
-
relativeSourceFilePath,
|
|
354
|
-
sourceFile,
|
|
355
|
-
prettierConfigPath: options.prettierConfigPath,
|
|
356
|
-
storiesFolder: options.storiesFolder
|
|
357
|
-
});
|
|
358
|
-
} else {
|
|
351
|
+
if (options.preset !== "react") {
|
|
359
352
|
throwErr({
|
|
360
353
|
errorCode: "EC02",
|
|
361
354
|
detail: `Preset ${options.preset} is not supported in this fork. Only "react" is supported.`
|
|
362
355
|
});
|
|
363
356
|
return;
|
|
364
357
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
358
|
+
const storyCode = genReactStoryFile({
|
|
359
|
+
componentName,
|
|
360
|
+
fileBase,
|
|
361
|
+
fileName,
|
|
362
|
+
path: id,
|
|
363
|
+
fileExt,
|
|
364
|
+
filePrefixExt,
|
|
365
|
+
relativeSourceFilePath,
|
|
366
|
+
sourceFile,
|
|
367
|
+
storiesFolder: options.storiesFolder
|
|
368
|
+
});
|
|
369
|
+
if (!storyCode) return null;
|
|
370
|
+
const parsed = path4.parse(id);
|
|
371
|
+
const storyFileName = `${fileName}.stories.tsx`;
|
|
372
|
+
let storiesFilePath;
|
|
374
373
|
if (options.storiesFolder) {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
);
|
|
381
|
-
storiesFolderPath = storiesFilePath.replace(
|
|
382
|
-
`${genStoryFileOptions.fileOptions.fileName}${genStoryFileOptions.generateOptions.fileExt}`,
|
|
383
|
-
options.storiesFolder || ""
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
const storiesFilePathFinal = options.storiesFolder ? storiesFilePathWithStoriesFolder : storiesFilePath;
|
|
387
|
-
const storyExists = fs.existsSync(storiesFilePathFinal);
|
|
388
|
-
if (!storyExists) {
|
|
389
|
-
if (options.storiesFolder) {
|
|
390
|
-
fs.mkdirSync(storiesFolderPath, { recursive: true });
|
|
391
|
-
}
|
|
392
|
-
fs.writeFileSync(
|
|
393
|
-
storiesFilePathFinal,
|
|
394
|
-
genStoryFileOptions.generateOptions.initialCode
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
const storiesProject = new Project();
|
|
398
|
-
const storiesSourceFile = storiesProject.addSourceFileAtPath(storiesFilePathFinal);
|
|
399
|
-
const meta = storiesSourceFile.getVariableDeclaration("meta");
|
|
400
|
-
if (!meta || !meta.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
401
|
-
throwErr({
|
|
402
|
-
errorCode: "EC05",
|
|
403
|
-
detail: `Could not find meta in file ${storiesSourceFile.getFilePath()}`
|
|
404
|
-
});
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const initializer = meta.getInitializerIfKindOrThrow(
|
|
408
|
-
SyntaxKind.ObjectLiteralExpression
|
|
409
|
-
);
|
|
410
|
-
if (!initializer) {
|
|
411
|
-
throwErr({ errorCode: "EC06" });
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
if (genStoryFileOptions.generateOptions.meta.render) {
|
|
415
|
-
let renderProperty = initializer.getProperty("render");
|
|
416
|
-
while (!renderProperty) {
|
|
417
|
-
initializer.addPropertyAssignment({
|
|
418
|
-
name: "render",
|
|
419
|
-
initializer: "() => {}"
|
|
420
|
-
});
|
|
421
|
-
renderProperty = initializer.getProperty("render");
|
|
422
|
-
}
|
|
423
|
-
renderProperty.set({
|
|
424
|
-
initializer: genStoryFileOptions.generateOptions.meta.render
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
if (genStoryFileOptions.generateOptions.meta.component) {
|
|
428
|
-
let componentProperty = initializer.getProperty("component");
|
|
429
|
-
while (!componentProperty) {
|
|
430
|
-
initializer.addPropertyAssignment({
|
|
431
|
-
name: "component",
|
|
432
|
-
initializer: "null"
|
|
433
|
-
});
|
|
434
|
-
componentProperty = initializer.getProperty("component");
|
|
435
|
-
}
|
|
436
|
-
componentProperty.set({
|
|
437
|
-
initializer: genStoryFileOptions.generateOptions.meta.component
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
if (genStoryFileOptions.generateOptions.meta.args) {
|
|
441
|
-
let argsProperty = initializer.getProperty("args");
|
|
442
|
-
while (!argsProperty) {
|
|
443
|
-
initializer.addPropertyAssignment({
|
|
444
|
-
name: "args",
|
|
445
|
-
initializer: "{}"
|
|
446
|
-
});
|
|
447
|
-
argsProperty = initializer.getProperty("args");
|
|
448
|
-
}
|
|
449
|
-
const argText = Object.entries(genStoryFileOptions.generateOptions.meta.args).map((x) => x.join(":")).join(", ");
|
|
450
|
-
argsProperty.set({ initializer: `{ ${argText} }` });
|
|
374
|
+
const storiesFolderPath = path4.join(parsed.dir, options.storiesFolder);
|
|
375
|
+
fs2.mkdirSync(storiesFolderPath, { recursive: true });
|
|
376
|
+
storiesFilePath = path4.join(storiesFolderPath, storyFileName);
|
|
377
|
+
} else {
|
|
378
|
+
storiesFilePath = path4.join(parsed.dir, storyFileName);
|
|
451
379
|
}
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
while (!argTypesProperty) {
|
|
455
|
-
initializer.addPropertyAssignment({
|
|
456
|
-
name: "argTypes",
|
|
457
|
-
initializer: "{}"
|
|
458
|
-
});
|
|
459
|
-
argTypesProperty = initializer.getProperty("argTypes");
|
|
460
|
-
}
|
|
461
|
-
const argTypesText = JSON.stringify(
|
|
462
|
-
genStoryFileOptions.generateOptions.meta.argTypes,
|
|
463
|
-
null,
|
|
464
|
-
""
|
|
465
|
-
);
|
|
466
|
-
argTypesProperty.set({ initializer: `${argTypesText}` });
|
|
380
|
+
if (!fs2.existsSync(storiesFilePath)) {
|
|
381
|
+
fs2.writeFileSync(storiesFilePath, storyCode);
|
|
467
382
|
}
|
|
468
|
-
|
|
469
|
-
const fileContent = fs.readFileSync(storiesFilePathFinal, "utf-8");
|
|
470
|
-
const config = await getPrettierConfig(
|
|
471
|
-
genStoryFileOptions.fileOptions.prettierConfigPath
|
|
472
|
-
);
|
|
473
|
-
const formattedContent = await prettier.format(fileContent, {
|
|
474
|
-
...config,
|
|
475
|
-
parser: "typescript"
|
|
476
|
-
});
|
|
477
|
-
fs.writeFileSync(storiesFilePathFinal, formattedContent);
|
|
383
|
+
return storiesFilePath;
|
|
478
384
|
} catch (err) {
|
|
479
385
|
console.warn(`[ASG] Failed to generate story for ${id}:`, err);
|
|
386
|
+
return null;
|
|
480
387
|
}
|
|
481
388
|
}
|
|
482
389
|
async function getAllFilePaths({
|
|
@@ -485,10 +392,10 @@ async function getAllFilePaths({
|
|
|
485
392
|
projectRootDir
|
|
486
393
|
}) {
|
|
487
394
|
const fullPatterns = patterns.map(
|
|
488
|
-
(p) =>
|
|
395
|
+
(p) => path4.join(projectRootDir, p).replace(/\\/g, "/")
|
|
489
396
|
);
|
|
490
397
|
const ignoreFullPatterns = ignorePatterns?.map(
|
|
491
|
-
(p) =>
|
|
398
|
+
(p) => path4.join(projectRootDir, p).replace(/\\/g, "/")
|
|
492
399
|
);
|
|
493
400
|
const filePaths = await glob(fullPatterns, {
|
|
494
401
|
ignore: ignoreFullPatterns,
|
|
@@ -497,12 +404,37 @@ async function getAllFilePaths({
|
|
|
497
404
|
return filePaths.map((p) => p.replace(/\\/g, "/"));
|
|
498
405
|
}
|
|
499
406
|
var PLUGIN_NAME = "auto-story-generator";
|
|
500
|
-
var DEFAULT_BATCH_SIZE =
|
|
501
|
-
var DEFAULT_CONCURRENCY =
|
|
407
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
408
|
+
var DEFAULT_CONCURRENCY = 8;
|
|
409
|
+
var CACHE_FILE_NAME = ".asg-cache.json";
|
|
502
410
|
var mtimeCache = /* @__PURE__ */ new Map();
|
|
411
|
+
function loadDiskCache(projectRootDir) {
|
|
412
|
+
try {
|
|
413
|
+
const cachePath = path4.join(projectRootDir, CACHE_FILE_NAME);
|
|
414
|
+
if (fs2.existsSync(cachePath)) {
|
|
415
|
+
const data = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
|
|
416
|
+
for (const [key, value] of Object.entries(data)) {
|
|
417
|
+
mtimeCache.set(key, value);
|
|
418
|
+
}
|
|
419
|
+
console.log(`[ASG] Loaded ${mtimeCache.size} entries from disk cache`);
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function saveDiskCache(projectRootDir) {
|
|
425
|
+
try {
|
|
426
|
+
const cachePath = path4.join(projectRootDir, CACHE_FILE_NAME);
|
|
427
|
+
const data = {};
|
|
428
|
+
for (const [key, value] of mtimeCache) {
|
|
429
|
+
data[key] = value;
|
|
430
|
+
}
|
|
431
|
+
fs2.writeFileSync(cachePath, JSON.stringify(data));
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
503
435
|
function hasFileChanged(filePath) {
|
|
504
436
|
try {
|
|
505
|
-
const stat =
|
|
437
|
+
const stat = fs2.statSync(filePath);
|
|
506
438
|
const mtime = stat.mtimeMs;
|
|
507
439
|
const cached = mtimeCache.get(filePath);
|
|
508
440
|
if (cached === mtime) return false;
|
|
@@ -514,24 +446,25 @@ function hasFileChanged(filePath) {
|
|
|
514
446
|
}
|
|
515
447
|
}
|
|
516
448
|
function getStoryFilePath(filePath, storiesFolder) {
|
|
517
|
-
const parsed =
|
|
449
|
+
const parsed = path4.parse(filePath);
|
|
518
450
|
const storyName = `${parsed.name}.stories.tsx`;
|
|
519
451
|
if (storiesFolder) {
|
|
520
|
-
return
|
|
452
|
+
return path4.join(parsed.dir, storiesFolder, storyName);
|
|
521
453
|
}
|
|
522
|
-
return
|
|
454
|
+
return path4.join(parsed.dir, storyName);
|
|
523
455
|
}
|
|
524
|
-
async function processBatch(files, options, projectRootDir, concurrency) {
|
|
456
|
+
async function processBatch(files, options, projectRootDir, concurrency, generatedFiles) {
|
|
525
457
|
let processed = 0;
|
|
526
458
|
for (let i = 0; i < files.length; i += concurrency) {
|
|
527
459
|
const chunk = files.slice(i, i + concurrency);
|
|
528
460
|
await Promise.all(
|
|
529
461
|
chunk.map(async (filePath) => {
|
|
530
|
-
await genStoryFile({
|
|
462
|
+
const result = await genStoryFile({
|
|
531
463
|
options,
|
|
532
464
|
id: filePath,
|
|
533
465
|
projectRootDir
|
|
534
466
|
});
|
|
467
|
+
if (result) generatedFiles.push(result);
|
|
535
468
|
processed++;
|
|
536
469
|
})
|
|
537
470
|
);
|
|
@@ -543,6 +476,7 @@ function createAutoStoryPlugin(options) {
|
|
|
543
476
|
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
544
477
|
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
545
478
|
let hasRun = false;
|
|
479
|
+
const allGeneratedFiles = [];
|
|
546
480
|
console.log("[ASG] Plugin created", {
|
|
547
481
|
preset: options.preset,
|
|
548
482
|
imports: options.imports,
|
|
@@ -558,6 +492,7 @@ function createAutoStoryPlugin(options) {
|
|
|
558
492
|
console.log("[ASG] Skipping \u2014 isGenerateStoriesFileAtBuild is false");
|
|
559
493
|
return;
|
|
560
494
|
}
|
|
495
|
+
loadDiskCache(projectRootDir);
|
|
561
496
|
const patterns = options.imports ?? ["src/**/*.tsx"];
|
|
562
497
|
const ignorePatterns = options.ignores ?? [];
|
|
563
498
|
console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
|
|
@@ -570,7 +505,7 @@ function createAutoStoryPlugin(options) {
|
|
|
570
505
|
if (filePath.includes(".stories")) return false;
|
|
571
506
|
if (options.cacheEnabled !== false) {
|
|
572
507
|
const storyPath = getStoryFilePath(filePath, options.storiesFolder);
|
|
573
|
-
const storyExists =
|
|
508
|
+
const storyExists = fs2.existsSync(storyPath);
|
|
574
509
|
if (!hasFileChanged(filePath) && storyExists) return false;
|
|
575
510
|
}
|
|
576
511
|
return true;
|
|
@@ -581,7 +516,13 @@ function createAutoStoryPlugin(options) {
|
|
|
581
516
|
let totalProcessed = 0;
|
|
582
517
|
for (let i = 0; i < filesToProcess.length; i += batchSize) {
|
|
583
518
|
const batch = filesToProcess.slice(i, i + batchSize);
|
|
584
|
-
const count = await processBatch(
|
|
519
|
+
const count = await processBatch(
|
|
520
|
+
batch,
|
|
521
|
+
options,
|
|
522
|
+
projectRootDir,
|
|
523
|
+
concurrency,
|
|
524
|
+
allGeneratedFiles
|
|
525
|
+
);
|
|
585
526
|
totalProcessed += count;
|
|
586
527
|
if (options.onProgress) {
|
|
587
528
|
options.onProgress(totalProcessed, filesToProcess.length);
|
|
@@ -590,14 +531,26 @@ function createAutoStoryPlugin(options) {
|
|
|
590
531
|
await new Promise((r) => setTimeout(r, 0));
|
|
591
532
|
}
|
|
592
533
|
}
|
|
534
|
+
saveDiskCache(projectRootDir);
|
|
593
535
|
console.log(`[ASG] Generated stories for ${totalProcessed} components`);
|
|
594
536
|
}
|
|
595
537
|
return {
|
|
596
538
|
name: PLUGIN_NAME,
|
|
597
539
|
// configureServer fires for Vite dev server (Storybook's use case)
|
|
598
|
-
|
|
540
|
+
// biome-ignore lint/suspicious/noExplicitAny: Vite server type varies across versions
|
|
541
|
+
async configureServer(server) {
|
|
599
542
|
console.log("[ASG] configureServer hook fired");
|
|
543
|
+
const beforeCount = allGeneratedFiles.length;
|
|
600
544
|
await generateAllStories();
|
|
545
|
+
if (allGeneratedFiles.length > beforeCount && allGeneratedFiles.length > 0) {
|
|
546
|
+
const firstFile = allGeneratedFiles[0];
|
|
547
|
+
if (firstFile && server?.watcher?.emit) {
|
|
548
|
+
console.log(
|
|
549
|
+
`[ASG] Triggering Storybook rescan (${allGeneratedFiles.length} stories)`
|
|
550
|
+
);
|
|
551
|
+
server.watcher.emit("change", firstFile);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
601
554
|
},
|
|
602
555
|
// buildStart fires for builds (fallback)
|
|
603
556
|
async buildStart() {
|
|
@@ -641,12 +594,12 @@ var parser = withDefaultConfig({
|
|
|
641
594
|
}
|
|
642
595
|
});
|
|
643
596
|
function resolveComponentPath(storyFilePath) {
|
|
644
|
-
const dir =
|
|
645
|
-
const parentDir =
|
|
646
|
-
const storyName =
|
|
597
|
+
const dir = path4.dirname(storyFilePath);
|
|
598
|
+
const parentDir = path4.dirname(dir);
|
|
599
|
+
const storyName = path4.basename(storyFilePath);
|
|
647
600
|
const componentName = storyName.replace(".stories.tsx", ".tsx");
|
|
648
|
-
const componentPath =
|
|
649
|
-
return
|
|
601
|
+
const componentPath = path4.join(parentDir, componentName);
|
|
602
|
+
return fs2.existsSync(componentPath) ? componentPath : null;
|
|
650
603
|
}
|
|
651
604
|
function generateArgTypes(componentPath) {
|
|
652
605
|
try {
|
|
@@ -687,7 +640,7 @@ function generateArgTypes(componentPath) {
|
|
|
687
640
|
}
|
|
688
641
|
function enrichStoryFile(storyFilePath) {
|
|
689
642
|
try {
|
|
690
|
-
const content =
|
|
643
|
+
const content = fs2.readFileSync(storyFilePath, "utf-8");
|
|
691
644
|
if (content.includes(FIXED_MARKER)) {
|
|
692
645
|
return;
|
|
693
646
|
}
|
|
@@ -706,8 +659,8 @@ function enrichStoryFile(storyFilePath) {
|
|
|
706
659
|
export default meta;`
|
|
707
660
|
);
|
|
708
661
|
if (enriched !== content) {
|
|
709
|
-
|
|
710
|
-
console.log(`[AutoStories] Enriched ${
|
|
662
|
+
fs2.writeFileSync(storyFilePath, enriched);
|
|
663
|
+
console.log(`[AutoStories] Enriched ${path4.basename(storyFilePath)} with argTypes`);
|
|
711
664
|
}
|
|
712
665
|
} catch (err) {
|
|
713
666
|
console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
|
|
@@ -735,7 +688,7 @@ function componentLocPlugin(options = {}) {
|
|
|
735
688
|
sourceFilename: filepath
|
|
736
689
|
});
|
|
737
690
|
let mutated = false;
|
|
738
|
-
const relativePath =
|
|
691
|
+
const relativePath = path4.relative(root, filepath);
|
|
739
692
|
traverse(ast, {
|
|
740
693
|
JSXElement(nodePath) {
|
|
741
694
|
const opening = nodePath.node.openingElement;
|
|
@@ -772,9 +725,9 @@ function componentLocPlugin(options = {}) {
|
|
|
772
725
|
}
|
|
773
726
|
};
|
|
774
727
|
}
|
|
775
|
-
var CACHE_DIR =
|
|
776
|
-
var SCREENSHOTS_DIR =
|
|
777
|
-
var MANIFEST_PATH =
|
|
728
|
+
var CACHE_DIR = path4.join(process.cwd(), ".storybook-cache");
|
|
729
|
+
var SCREENSHOTS_DIR = path4.join(CACHE_DIR, "screenshots");
|
|
730
|
+
var MANIFEST_PATH = path4.join(CACHE_DIR, "manifest.json");
|
|
778
731
|
var VIEWPORT_WIDTH = 1920;
|
|
779
732
|
var VIEWPORT_HEIGHT = 1080;
|
|
780
733
|
var MIN_COMPONENT_WIDTH = 420;
|
|
@@ -782,30 +735,30 @@ var MIN_COMPONENT_HEIGHT = 280;
|
|
|
782
735
|
|
|
783
736
|
// src/utils/fileSystem/fileSystem.ts
|
|
784
737
|
function ensureCacheDirectories() {
|
|
785
|
-
if (!
|
|
786
|
-
|
|
738
|
+
if (!fs2.existsSync(CACHE_DIR)) {
|
|
739
|
+
fs2.mkdirSync(CACHE_DIR, { recursive: true });
|
|
787
740
|
}
|
|
788
|
-
if (!
|
|
789
|
-
|
|
741
|
+
if (!fs2.existsSync(SCREENSHOTS_DIR)) {
|
|
742
|
+
fs2.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
790
743
|
}
|
|
791
744
|
}
|
|
792
745
|
function computeFileHash(filePath) {
|
|
793
|
-
if (!
|
|
746
|
+
if (!fs2.existsSync(filePath)) {
|
|
794
747
|
return "";
|
|
795
748
|
}
|
|
796
|
-
const content =
|
|
749
|
+
const content = fs2.readFileSync(filePath, "utf-8");
|
|
797
750
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
798
751
|
}
|
|
799
752
|
function loadManifest() {
|
|
800
|
-
if (
|
|
801
|
-
const content =
|
|
753
|
+
if (fs2.existsSync(MANIFEST_PATH)) {
|
|
754
|
+
const content = fs2.readFileSync(MANIFEST_PATH, "utf-8");
|
|
802
755
|
return JSON.parse(content);
|
|
803
756
|
}
|
|
804
757
|
return { stories: {} };
|
|
805
758
|
}
|
|
806
759
|
function saveManifest(manifest) {
|
|
807
760
|
ensureCacheDirectories();
|
|
808
|
-
|
|
761
|
+
fs2.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
809
762
|
}
|
|
810
763
|
function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
|
|
811
764
|
const manifest = loadManifest();
|
|
@@ -831,8 +784,8 @@ async function getBrowser() {
|
|
|
831
784
|
return browser;
|
|
832
785
|
}
|
|
833
786
|
function getScreenshotPath(storyId, theme) {
|
|
834
|
-
const storyDir =
|
|
835
|
-
return
|
|
787
|
+
const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
|
|
788
|
+
return path4.join(storyDir, `${theme}.png`);
|
|
836
789
|
}
|
|
837
790
|
async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
838
791
|
const browser2 = await getBrowser();
|
|
@@ -919,9 +872,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
919
872
|
async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
|
|
920
873
|
try {
|
|
921
874
|
ensureCacheDirectories();
|
|
922
|
-
const storyDir =
|
|
923
|
-
if (!
|
|
924
|
-
|
|
875
|
+
const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
|
|
876
|
+
if (!fs2.existsSync(storyDir)) {
|
|
877
|
+
fs2.mkdirSync(storyDir, { recursive: true });
|
|
925
878
|
}
|
|
926
879
|
const screenshotPath = getScreenshotPath(storyId, theme);
|
|
927
880
|
const { buffer, boundingBox } = await captureScreenshotBuffer(
|
|
@@ -932,7 +885,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
932
885
|
storybookUrl,
|
|
933
886
|
timeoutMs
|
|
934
887
|
);
|
|
935
|
-
|
|
888
|
+
fs2.writeFileSync(screenshotPath, buffer);
|
|
936
889
|
return { path: screenshotPath, boundingBox };
|
|
937
890
|
} catch (error) {
|
|
938
891
|
console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
|
|
@@ -959,7 +912,7 @@ async function fetchStorybookIndex() {
|
|
|
959
912
|
}
|
|
960
913
|
function getStoriesForFile(filePath) {
|
|
961
914
|
if (!cachedIndex) return [];
|
|
962
|
-
const fileName =
|
|
915
|
+
const fileName = path4.basename(filePath);
|
|
963
916
|
return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
|
|
964
917
|
}
|
|
965
918
|
async function regenerateScreenshotsForFiles(files) {
|
|
@@ -1121,7 +1074,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1121
1074
|
}
|
|
1122
1075
|
if (req.url === "/onbook-index.json") {
|
|
1123
1076
|
console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
|
|
1124
|
-
const manifestPath =
|
|
1077
|
+
const manifestPath = path4.join(process.cwd(), ".storybook-cache", "manifest.json");
|
|
1125
1078
|
const cacheBuster = Date.now();
|
|
1126
1079
|
console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
|
|
1127
1080
|
fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
|
|
@@ -1136,9 +1089,20 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1136
1089
|
ok: response.ok,
|
|
1137
1090
|
statusText: response.statusText
|
|
1138
1091
|
});
|
|
1092
|
+
if (!response.ok) {
|
|
1093
|
+
console.log("[STORYBOOK_PLUGIN] Index not ready yet", {
|
|
1094
|
+
status: response.status
|
|
1095
|
+
});
|
|
1096
|
+
res.statusCode = 503;
|
|
1097
|
+
res.setHeader("Content-Type", "application/json");
|
|
1098
|
+
res.setHeader("Retry-After", "10");
|
|
1099
|
+
res.end(JSON.stringify({ code: "INDEX_NOT_READY" }));
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1139
1102
|
return response.json();
|
|
1140
1103
|
}).then((indexData) => {
|
|
1141
|
-
|
|
1104
|
+
if (!indexData) return;
|
|
1105
|
+
const manifest = fs2.existsSync(manifestPath) ? JSON.parse(fs2.readFileSync(manifestPath, "utf-8")) : { stories: {} };
|
|
1142
1106
|
const defaultBoundingBox = { width: 1920, height: 1080 };
|
|
1143
1107
|
for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
|
|
1144
1108
|
const manifestEntry = manifest.stories?.[storyId];
|
|
@@ -1156,15 +1120,17 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1156
1120
|
res.setHeader("Expires", "0");
|
|
1157
1121
|
res.end(JSON.stringify(indexData));
|
|
1158
1122
|
}).catch((error) => {
|
|
1159
|
-
console.
|
|
1160
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1161
|
-
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
1162
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
1123
|
+
console.log("[STORYBOOK_PLUGIN] Index not available", {
|
|
1124
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1163
1125
|
});
|
|
1164
|
-
res.statusCode =
|
|
1126
|
+
res.statusCode = 503;
|
|
1165
1127
|
res.setHeader("Content-Type", "application/json");
|
|
1128
|
+
res.setHeader("Retry-After", "10");
|
|
1166
1129
|
res.end(
|
|
1167
|
-
JSON.stringify({
|
|
1130
|
+
JSON.stringify({
|
|
1131
|
+
code: "INDEX_NOT_READY",
|
|
1132
|
+
details: String(error)
|
|
1133
|
+
})
|
|
1168
1134
|
);
|
|
1169
1135
|
});
|
|
1170
1136
|
return;
|
|
@@ -1206,7 +1172,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1206
1172
|
return;
|
|
1207
1173
|
}
|
|
1208
1174
|
if (req.url?.startsWith("/screenshots/")) {
|
|
1209
|
-
const screenshotPath =
|
|
1175
|
+
const screenshotPath = path4.join(
|
|
1210
1176
|
process.cwd(),
|
|
1211
1177
|
".storybook-cache",
|
|
1212
1178
|
req.url.replace("/screenshots/", "screenshots/")
|
|
@@ -1215,11 +1181,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1215
1181
|
const storyId = urlParts[0];
|
|
1216
1182
|
const themeFile = urlParts[1];
|
|
1217
1183
|
const theme = themeFile?.replace(".png", "");
|
|
1218
|
-
if (
|
|
1184
|
+
if (fs2.existsSync(screenshotPath)) {
|
|
1219
1185
|
res.setHeader("Content-Type", "image/png");
|
|
1220
1186
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1221
1187
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
1222
|
-
|
|
1188
|
+
fs2.createReadStream(screenshotPath).pipe(res);
|
|
1223
1189
|
return;
|
|
1224
1190
|
}
|
|
1225
1191
|
if (storyId && theme && (theme === "light" || theme === "dark")) {
|
|
@@ -1227,16 +1193,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
|
|
|
1227
1193
|
`[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
|
|
1228
1194
|
);
|
|
1229
1195
|
captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
|
|
1230
|
-
const storyDir =
|
|
1196
|
+
const storyDir = path4.join(
|
|
1231
1197
|
process.cwd(),
|
|
1232
1198
|
".storybook-cache",
|
|
1233
1199
|
"screenshots",
|
|
1234
1200
|
storyId
|
|
1235
1201
|
);
|
|
1236
|
-
if (!
|
|
1237
|
-
|
|
1202
|
+
if (!fs2.existsSync(storyDir)) {
|
|
1203
|
+
fs2.mkdirSync(storyDir, { recursive: true });
|
|
1238
1204
|
}
|
|
1239
|
-
|
|
1205
|
+
fs2.writeFileSync(screenshotPath, buffer);
|
|
1240
1206
|
res.setHeader("Content-Type", "image/png");
|
|
1241
1207
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1242
1208
|
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
@@ -1328,7 +1294,14 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
1328
1294
|
"src/**/*.spec.tsx",
|
|
1329
1295
|
"src/**/*.spec.ts",
|
|
1330
1296
|
"node_modules/**",
|
|
1331
|
-
"**/.onlook-stories/**"
|
|
1297
|
+
"**/.onlook-stories/**",
|
|
1298
|
+
// Next.js route files (not renderable as Storybook stories)
|
|
1299
|
+
"src/**/page.tsx",
|
|
1300
|
+
"src/**/layout.tsx",
|
|
1301
|
+
"src/**/loading.tsx",
|
|
1302
|
+
"src/**/error.tsx",
|
|
1303
|
+
"src/**/not-found.tsx",
|
|
1304
|
+
"src/**/template.tsx"
|
|
1332
1305
|
];
|
|
1333
1306
|
console.log("[STORYBOOK_PLUGIN] Auto-story generation enabled", {
|
|
1334
1307
|
imports,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlook/storybook-plugin",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"onlook-storybook": "./dist/cli/index.js"
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"glob": "^10.3.10",
|
|
41
41
|
"minimatch": "^9.0.3",
|
|
42
42
|
"playwright": "^1.52.0",
|
|
43
|
-
"prettier": "^3.2.5",
|
|
44
43
|
"react-docgen-typescript": "^2.4.0",
|
|
45
44
|
"ts-morph": "^21.0.1"
|
|
46
45
|
},
|