@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.
Files changed (2) hide show
  1. package/dist/index.js +267 -294
  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
  );
@@ -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 = fs.existsSync(storyPath);
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(batch, options, projectRootDir, concurrency);
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
- async configureServer() {
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 = path3.dirname(storyFilePath);
645
- const parentDir = path3.dirname(dir);
646
- const storyName = path3.basename(storyFilePath);
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 = path3.join(parentDir, componentName);
649
- return fs.existsSync(componentPath) ? componentPath : null;
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 = fs.readFileSync(storyFilePath, "utf-8");
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
- fs.writeFileSync(storyFilePath, enriched);
710
- console.log(`[AutoStories] Enriched ${path3.basename(storyFilePath)} with argTypes`);
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 = path3.relative(root, filepath);
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 = path3.join(process.cwd(), ".storybook-cache");
776
- var SCREENSHOTS_DIR = path3.join(CACHE_DIR, "screenshots");
777
- 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");
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 (!fs.existsSync(CACHE_DIR)) {
786
- fs.mkdirSync(CACHE_DIR, { recursive: true });
738
+ if (!fs2.existsSync(CACHE_DIR)) {
739
+ fs2.mkdirSync(CACHE_DIR, { recursive: true });
787
740
  }
788
- if (!fs.existsSync(SCREENSHOTS_DIR)) {
789
- fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
741
+ if (!fs2.existsSync(SCREENSHOTS_DIR)) {
742
+ fs2.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
790
743
  }
791
744
  }
792
745
  function computeFileHash(filePath) {
793
- if (!fs.existsSync(filePath)) {
746
+ if (!fs2.existsSync(filePath)) {
794
747
  return "";
795
748
  }
796
- const content = fs.readFileSync(filePath, "utf-8");
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 (fs.existsSync(MANIFEST_PATH)) {
801
- const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
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
- fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
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 = path3.join(SCREENSHOTS_DIR, storyId);
835
- return path3.join(storyDir, `${theme}.png`);
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 = path3.join(SCREENSHOTS_DIR, storyId);
923
- if (!fs.existsSync(storyDir)) {
924
- fs.mkdirSync(storyDir, { recursive: true });
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
- fs.writeFileSync(screenshotPath, buffer);
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 = path3.basename(filePath);
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 = path3.join(process.cwd(), ".storybook-cache", "manifest.json");
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
- 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: {} };
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.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
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 = 500;
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({ error: "Failed to fetch index", details: String(error) })
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 = path3.join(
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 (fs.existsSync(screenshotPath)) {
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
- fs.createReadStream(screenshotPath).pipe(res);
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 = path3.join(
1196
+ const storyDir = path4.join(
1231
1197
  process.cwd(),
1232
1198
  ".storybook-cache",
1233
1199
  "screenshots",
1234
1200
  storyId
1235
1201
  );
1236
- if (!fs.existsSync(storyDir)) {
1237
- fs.mkdirSync(storyDir, { recursive: true });
1202
+ if (!fs2.existsSync(storyDir)) {
1203
+ fs2.mkdirSync(storyDir, { recursive: true });
1238
1204
  }
1239
- fs.writeFileSync(screenshotPath, buffer);
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.5",
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
  },