@onlook/storybook-plugin 0.4.0-beta.4 → 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.
Files changed (2) hide show
  1. package/dist/index.js +313 -328
  2. package/package.json +1 -2
package/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
- import fs, { existsSync } from 'fs';
2
- import path3, { dirname, join, relative } from 'path';
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 * as prettier from 'prettier';
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 = path3.parse(componentDir);
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
- async function genReactStoryFile({
191
- componentName,
192
- fileBase,
193
- fileName,
194
- filePrefixExt,
195
- path: path42,
196
- fileExt,
197
- relativeSourceFilePath,
198
- sourceFile,
199
- prettierConfigPath,
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
- errorCode: "EC03"
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
- errorCode: "EC04"
216
- });
217
- return;
206
+ throwErr({ errorCode: "EC04" });
207
+ return null;
218
208
  }
219
- const defaultExportDeclaration = sourceFile.getExportedDeclarations();
209
+ const exportedDeclarations = sourceFile.getExportedDeclarations();
220
210
  let isDefaultExportComponent = false;
221
- defaultExportDeclaration.forEach((declaration, exportName) => {
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 initialCode = `
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
- ${isDefaultExportComponent ? `import ${pascalComponentName} from "${pathToComponent}${fileName}${filePrefixExt || ""}";` : `import { ${pascalComponentName} } from "${pathToComponent}${fileName}${filePrefixExt || ""}";`}
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: "components/${pascalComponentName}",
235
- component: (args) => <${componentName} {...args} />,
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
- const componentCode = `${pascalComponentName}`;
247
- const args = {};
248
- propTypes.forEach((prop) => {
249
- if (prop.isOptional) return args[prop.name] = "undefined";
250
- let value = prop.value.length > 0 ? `"${prop.value[0]}"` : "undefined";
251
- if (prop.type.includes("boolean")) value = true;
252
- args[prop.name] = value;
253
- });
254
- const argTypes = {};
255
- propTypes.forEach((prop) => {
256
- if (prop.type[0] === "boolean") {
257
- return argTypes[prop.name] = {
258
- control: "boolean"
259
- };
260
- }
261
- if (prop.type[0] === "object") {
262
- return argTypes[prop.name] = {
263
- control: "object"
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 = fs.readFileSync(id, "utf-8");
348
+ const sourceCode = fs2.readFileSync(id, "utf-8");
342
349
  const sourceProject = createLightProject();
343
350
  const sourceFile = sourceProject.createSourceFile(fileBase || "temp.tsx", sourceCode);
344
- let genStoryFileOptions;
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
- if (!genStoryFileOptions) {
366
- return;
367
- }
368
- const storiesFilePath = genStoryFileOptions.fileOptions.path.replace(
369
- genStoryFileOptions.fileOptions.filePrefixExt ? genStoryFileOptions.fileOptions.filePrefixExt : `${genStoryFileOptions.fileOptions.fileExt}`,
370
- genStoryFileOptions.generateOptions.fileExt
371
- );
372
- let storiesFolderPath = "";
373
- let storiesFilePathWithStoriesFolder = "";
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 splitStoriesFilePath = storiesFilePath.split("/");
376
- const fileNameWithStoriesFolder = `${options.storiesFolder}/${splitStoriesFilePath[splitStoriesFilePath.length - 1]}`;
377
- storiesFilePathWithStoriesFolder = storiesFilePath.replace(
378
- `${genStoryFileOptions.fileOptions.fileName}${genStoryFileOptions.generateOptions.fileExt}`,
379
- fileNameWithStoriesFolder
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 (genStoryFileOptions.generateOptions.meta.argTypes) {
453
- let argTypesProperty = initializer.getProperty("argTypes");
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
- await storiesSourceFile.save();
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) => path3.join(projectRootDir, p).replace(/\\/g, "/")
395
+ (p) => path4.join(projectRootDir, p).replace(/\\/g, "/")
489
396
  );
490
397
  const ignoreFullPatterns = ignorePatterns?.map(
491
- (p) => path3.join(projectRootDir, p).replace(/\\/g, "/")
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 = 20;
501
- var DEFAULT_CONCURRENCY = 4;
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 = fs.statSync(filePath);
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 = path3.parse(filePath);
449
+ const parsed = path4.parse(filePath);
518
450
  const storyName = `${parsed.name}.stories.tsx`;
519
451
  if (storiesFolder) {
520
- return path3.join(parsed.dir, storiesFolder, storyName);
452
+ return path4.join(parsed.dir, storiesFolder, storyName);
521
453
  }
522
- return path3.join(parsed.dir, storyName);
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
  );
@@ -542,6 +475,8 @@ function createAutoStoryPlugin(options) {
542
475
  const projectRootDir = (options.projectRoot ?? process.cwd()).replace(/\\/g, "/");
543
476
  const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
544
477
  const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
478
+ let hasRun = false;
479
+ const allGeneratedFiles = [];
545
480
  console.log("[ASG] Plugin created", {
546
481
  preset: options.preset,
547
482
  imports: options.imports,
@@ -550,47 +485,77 @@ function createAutoStoryPlugin(options) {
550
485
  projectRoot: projectRootDir,
551
486
  cwd: process.cwd()
552
487
  });
553
- return {
554
- name: PLUGIN_NAME,
555
- async buildStart() {
556
- console.log("[ASG] buildStart hook fired");
557
- if (!options.isGenerateStoriesFileAtBuild) {
558
- console.log("[ASG] Skipping \u2014 isGenerateStoriesFileAtBuild is false");
559
- return;
488
+ async function generateAllStories() {
489
+ if (hasRun) return;
490
+ hasRun = true;
491
+ if (!options.isGenerateStoriesFileAtBuild) {
492
+ console.log("[ASG] Skipping \u2014 isGenerateStoriesFileAtBuild is false");
493
+ return;
494
+ }
495
+ loadDiskCache(projectRootDir);
496
+ const patterns = options.imports ?? ["src/**/*.tsx"];
497
+ const ignorePatterns = options.ignores ?? [];
498
+ console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
499
+ const allFiles = await getAllFilePaths({
500
+ patterns,
501
+ ignorePatterns,
502
+ projectRootDir
503
+ });
504
+ const filesToProcess = allFiles.filter((filePath) => {
505
+ if (filePath.includes(".stories")) return false;
506
+ if (options.cacheEnabled !== false) {
507
+ const storyPath = getStoryFilePath(filePath, options.storiesFolder);
508
+ const storyExists = fs2.existsSync(storyPath);
509
+ if (!hasFileChanged(filePath) && storyExists) return false;
560
510
  }
561
- const patterns = options.imports ?? ["src/**/*.tsx"];
562
- const ignorePatterns = options.ignores ?? [];
563
- console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
564
- const allFiles = await getAllFilePaths({
565
- patterns,
566
- ignorePatterns,
567
- projectRootDir
568
- });
569
- const filesToProcess = allFiles.filter((filePath) => {
570
- if (filePath.includes(".stories")) return false;
571
- if (options.cacheEnabled !== false) {
572
- const storyPath = getStoryFilePath(filePath, options.storiesFolder);
573
- const storyExists = fs.existsSync(storyPath);
574
- if (!hasFileChanged(filePath) && storyExists) return false;
575
- }
576
- return true;
577
- });
578
- console.log(
579
- `[ASG] Found ${allFiles.length} files, ${filesToProcess.length} need processing`
511
+ return true;
512
+ });
513
+ console.log(
514
+ `[ASG] Found ${allFiles.length} files, ${filesToProcess.length} need processing`
515
+ );
516
+ let totalProcessed = 0;
517
+ for (let i = 0; i < filesToProcess.length; i += batchSize) {
518
+ const batch = filesToProcess.slice(i, i + batchSize);
519
+ const count = await processBatch(
520
+ batch,
521
+ options,
522
+ projectRootDir,
523
+ concurrency,
524
+ allGeneratedFiles
580
525
  );
581
- let totalProcessed = 0;
582
- for (let i = 0; i < filesToProcess.length; i += batchSize) {
583
- const batch = filesToProcess.slice(i, i + batchSize);
584
- const count = await processBatch(batch, options, projectRootDir, concurrency);
585
- totalProcessed += count;
586
- if (options.onProgress) {
587
- options.onProgress(totalProcessed, filesToProcess.length);
588
- }
589
- if (i + batchSize < filesToProcess.length) {
590
- await new Promise((r) => setTimeout(r, 0));
526
+ totalProcessed += count;
527
+ if (options.onProgress) {
528
+ options.onProgress(totalProcessed, filesToProcess.length);
529
+ }
530
+ if (i + batchSize < filesToProcess.length) {
531
+ await new Promise((r) => setTimeout(r, 0));
532
+ }
533
+ }
534
+ saveDiskCache(projectRootDir);
535
+ console.log(`[ASG] Generated stories for ${totalProcessed} components`);
536
+ }
537
+ return {
538
+ name: PLUGIN_NAME,
539
+ // configureServer fires for Vite dev server (Storybook's use case)
540
+ // biome-ignore lint/suspicious/noExplicitAny: Vite server type varies across versions
541
+ async configureServer(server) {
542
+ console.log("[ASG] configureServer hook fired");
543
+ const beforeCount = allGeneratedFiles.length;
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);
591
552
  }
592
553
  }
593
- console.log(`[ASG] Generated stories for ${totalProcessed} components`);
554
+ },
555
+ // buildStart fires for builds (fallback)
556
+ async buildStart() {
557
+ console.log("[ASG] buildStart hook fired");
558
+ await generateAllStories();
594
559
  },
595
560
  async watchChange(id, change) {
596
561
  if (change.event === "delete") return;
@@ -629,12 +594,12 @@ var parser = withDefaultConfig({
629
594
  }
630
595
  });
631
596
  function resolveComponentPath(storyFilePath) {
632
- const dir = path3.dirname(storyFilePath);
633
- const parentDir = path3.dirname(dir);
634
- const storyName = path3.basename(storyFilePath);
597
+ const dir = path4.dirname(storyFilePath);
598
+ const parentDir = path4.dirname(dir);
599
+ const storyName = path4.basename(storyFilePath);
635
600
  const componentName = storyName.replace(".stories.tsx", ".tsx");
636
- const componentPath = path3.join(parentDir, componentName);
637
- return fs.existsSync(componentPath) ? componentPath : null;
601
+ const componentPath = path4.join(parentDir, componentName);
602
+ return fs2.existsSync(componentPath) ? componentPath : null;
638
603
  }
639
604
  function generateArgTypes(componentPath) {
640
605
  try {
@@ -675,7 +640,7 @@ function generateArgTypes(componentPath) {
675
640
  }
676
641
  function enrichStoryFile(storyFilePath) {
677
642
  try {
678
- const content = fs.readFileSync(storyFilePath, "utf-8");
643
+ const content = fs2.readFileSync(storyFilePath, "utf-8");
679
644
  if (content.includes(FIXED_MARKER)) {
680
645
  return;
681
646
  }
@@ -694,8 +659,8 @@ function enrichStoryFile(storyFilePath) {
694
659
  export default meta;`
695
660
  );
696
661
  if (enriched !== content) {
697
- fs.writeFileSync(storyFilePath, enriched);
698
- console.log(`[AutoStories] Enriched ${path3.basename(storyFilePath)} with argTypes`);
662
+ fs2.writeFileSync(storyFilePath, enriched);
663
+ console.log(`[AutoStories] Enriched ${path4.basename(storyFilePath)} with argTypes`);
699
664
  }
700
665
  } catch (err) {
701
666
  console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
@@ -723,7 +688,7 @@ function componentLocPlugin(options = {}) {
723
688
  sourceFilename: filepath
724
689
  });
725
690
  let mutated = false;
726
- const relativePath = path3.relative(root, filepath);
691
+ const relativePath = path4.relative(root, filepath);
727
692
  traverse(ast, {
728
693
  JSXElement(nodePath) {
729
694
  const opening = nodePath.node.openingElement;
@@ -760,9 +725,9 @@ function componentLocPlugin(options = {}) {
760
725
  }
761
726
  };
762
727
  }
763
- var CACHE_DIR = path3.join(process.cwd(), ".storybook-cache");
764
- var SCREENSHOTS_DIR = path3.join(CACHE_DIR, "screenshots");
765
- var MANIFEST_PATH = path3.join(CACHE_DIR, "manifest.json");
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");
766
731
  var VIEWPORT_WIDTH = 1920;
767
732
  var VIEWPORT_HEIGHT = 1080;
768
733
  var MIN_COMPONENT_WIDTH = 420;
@@ -770,30 +735,30 @@ var MIN_COMPONENT_HEIGHT = 280;
770
735
 
771
736
  // src/utils/fileSystem/fileSystem.ts
772
737
  function ensureCacheDirectories() {
773
- if (!fs.existsSync(CACHE_DIR)) {
774
- fs.mkdirSync(CACHE_DIR, { recursive: true });
738
+ if (!fs2.existsSync(CACHE_DIR)) {
739
+ fs2.mkdirSync(CACHE_DIR, { recursive: true });
775
740
  }
776
- if (!fs.existsSync(SCREENSHOTS_DIR)) {
777
- fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
741
+ if (!fs2.existsSync(SCREENSHOTS_DIR)) {
742
+ fs2.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
778
743
  }
779
744
  }
780
745
  function computeFileHash(filePath) {
781
- if (!fs.existsSync(filePath)) {
746
+ if (!fs2.existsSync(filePath)) {
782
747
  return "";
783
748
  }
784
- const content = fs.readFileSync(filePath, "utf-8");
749
+ const content = fs2.readFileSync(filePath, "utf-8");
785
750
  return crypto.createHash("sha256").update(content).digest("hex");
786
751
  }
787
752
  function loadManifest() {
788
- if (fs.existsSync(MANIFEST_PATH)) {
789
- const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
753
+ if (fs2.existsSync(MANIFEST_PATH)) {
754
+ const content = fs2.readFileSync(MANIFEST_PATH, "utf-8");
790
755
  return JSON.parse(content);
791
756
  }
792
757
  return { stories: {} };
793
758
  }
794
759
  function saveManifest(manifest) {
795
760
  ensureCacheDirectories();
796
- fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
761
+ fs2.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
797
762
  }
798
763
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
799
764
  const manifest = loadManifest();
@@ -819,8 +784,8 @@ async function getBrowser() {
819
784
  return browser;
820
785
  }
821
786
  function getScreenshotPath(storyId, theme) {
822
- const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
823
- return path3.join(storyDir, `${theme}.png`);
787
+ const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
788
+ return path4.join(storyDir, `${theme}.png`);
824
789
  }
825
790
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
826
791
  const browser2 = await getBrowser();
@@ -907,9 +872,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
907
872
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
908
873
  try {
909
874
  ensureCacheDirectories();
910
- const storyDir = path3.join(SCREENSHOTS_DIR, storyId);
911
- if (!fs.existsSync(storyDir)) {
912
- fs.mkdirSync(storyDir, { recursive: true });
875
+ const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
876
+ if (!fs2.existsSync(storyDir)) {
877
+ fs2.mkdirSync(storyDir, { recursive: true });
913
878
  }
914
879
  const screenshotPath = getScreenshotPath(storyId, theme);
915
880
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -920,7 +885,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
920
885
  storybookUrl,
921
886
  timeoutMs
922
887
  );
923
- fs.writeFileSync(screenshotPath, buffer);
888
+ fs2.writeFileSync(screenshotPath, buffer);
924
889
  return { path: screenshotPath, boundingBox };
925
890
  } catch (error) {
926
891
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -947,7 +912,7 @@ async function fetchStorybookIndex() {
947
912
  }
948
913
  function getStoriesForFile(filePath) {
949
914
  if (!cachedIndex) return [];
950
- const fileName = path3.basename(filePath);
915
+ const fileName = path4.basename(filePath);
951
916
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
952
917
  }
953
918
  async function regenerateScreenshotsForFiles(files) {
@@ -1109,7 +1074,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1109
1074
  }
1110
1075
  if (req.url === "/onbook-index.json") {
1111
1076
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
1112
- const manifestPath = path3.join(process.cwd(), ".storybook-cache", "manifest.json");
1077
+ const manifestPath = path4.join(process.cwd(), ".storybook-cache", "manifest.json");
1113
1078
  const cacheBuster = Date.now();
1114
1079
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
1115
1080
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -1124,9 +1089,20 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1124
1089
  ok: response.ok,
1125
1090
  statusText: response.statusText
1126
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
+ }
1127
1102
  return response.json();
1128
1103
  }).then((indexData) => {
1129
- const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf-8")) : { stories: {} };
1104
+ if (!indexData) return;
1105
+ const manifest = fs2.existsSync(manifestPath) ? JSON.parse(fs2.readFileSync(manifestPath, "utf-8")) : { stories: {} };
1130
1106
  const defaultBoundingBox = { width: 1920, height: 1080 };
1131
1107
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
1132
1108
  const manifestEntry = manifest.stories?.[storyId];
@@ -1144,15 +1120,17 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1144
1120
  res.setHeader("Expires", "0");
1145
1121
  res.end(JSON.stringify(indexData));
1146
1122
  }).catch((error) => {
1147
- console.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
1148
- error: error instanceof Error ? error.message : String(error),
1149
- errorType: error instanceof Error ? error.constructor.name : typeof error,
1150
- 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)
1151
1125
  });
1152
- res.statusCode = 500;
1126
+ res.statusCode = 503;
1153
1127
  res.setHeader("Content-Type", "application/json");
1128
+ res.setHeader("Retry-After", "10");
1154
1129
  res.end(
1155
- JSON.stringify({ error: "Failed to fetch index", details: String(error) })
1130
+ JSON.stringify({
1131
+ code: "INDEX_NOT_READY",
1132
+ details: String(error)
1133
+ })
1156
1134
  );
1157
1135
  });
1158
1136
  return;
@@ -1194,7 +1172,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1194
1172
  return;
1195
1173
  }
1196
1174
  if (req.url?.startsWith("/screenshots/")) {
1197
- const screenshotPath = path3.join(
1175
+ const screenshotPath = path4.join(
1198
1176
  process.cwd(),
1199
1177
  ".storybook-cache",
1200
1178
  req.url.replace("/screenshots/", "screenshots/")
@@ -1203,11 +1181,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1203
1181
  const storyId = urlParts[0];
1204
1182
  const themeFile = urlParts[1];
1205
1183
  const theme = themeFile?.replace(".png", "");
1206
- if (fs.existsSync(screenshotPath)) {
1184
+ if (fs2.existsSync(screenshotPath)) {
1207
1185
  res.setHeader("Content-Type", "image/png");
1208
1186
  res.setHeader("Access-Control-Allow-Origin", "*");
1209
1187
  res.setHeader("Cache-Control", "public, max-age=3600");
1210
- fs.createReadStream(screenshotPath).pipe(res);
1188
+ fs2.createReadStream(screenshotPath).pipe(res);
1211
1189
  return;
1212
1190
  }
1213
1191
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -1215,16 +1193,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1215
1193
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1216
1194
  );
1217
1195
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1218
- const storyDir = path3.join(
1196
+ const storyDir = path4.join(
1219
1197
  process.cwd(),
1220
1198
  ".storybook-cache",
1221
1199
  "screenshots",
1222
1200
  storyId
1223
1201
  );
1224
- if (!fs.existsSync(storyDir)) {
1225
- fs.mkdirSync(storyDir, { recursive: true });
1202
+ if (!fs2.existsSync(storyDir)) {
1203
+ fs2.mkdirSync(storyDir, { recursive: true });
1226
1204
  }
1227
- fs.writeFileSync(screenshotPath, buffer);
1205
+ fs2.writeFileSync(screenshotPath, buffer);
1228
1206
  res.setHeader("Content-Type", "image/png");
1229
1207
  res.setHeader("Access-Control-Allow-Origin", "*");
1230
1208
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -1316,7 +1294,14 @@ function storybookOnlookPlugin(options = {}) {
1316
1294
  "src/**/*.spec.tsx",
1317
1295
  "src/**/*.spec.ts",
1318
1296
  "node_modules/**",
1319
- "**/.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"
1320
1305
  ];
1321
1306
  console.log("[STORYBOOK_PLUGIN] Auto-story generation enabled", {
1322
1307
  imports,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.4.0-beta.4",
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
  },