@onlook/storybook-plugin 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,10 +61,16 @@ Generate screenshots for all stories:
61
61
 
62
62
  ```bash
63
63
  # With Storybook already running
64
- npx onlook-storybook generate-screenshots
64
+ npx generate-screenshots
65
65
 
66
- # Specify a custom Storybook directory
67
- npx onlook-storybook generate-screenshots -d ./path/to/storybook
66
+ # Start Storybook automatically
67
+ npx generate-screenshots --start
68
+
69
+ # Custom Storybook URL
70
+ npx generate-screenshots --url http://localhost:9009
71
+
72
+ # Custom start command
73
+ npx generate-screenshots --start --cmd "bun run storybook"
68
74
  ```
69
75
 
70
76
  Screenshots are saved to `.storybook-cache/screenshots/`.
package/dist/cli/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { command, string, run } from '@drizzle-team/brocli';
2
+ import { command, string, boolean, run } from '@drizzle-team/brocli';
3
3
  import { spawn } from 'child_process';
4
4
  import path, { resolve, join, dirname } from 'path';
5
5
  import crypto from 'crypto';
@@ -79,7 +79,11 @@ function getScreenshotPath(storyId, theme) {
79
79
  const storyDir = path.join(SCREENSHOTS_DIR, storyId);
80
80
  return path.join(storyDir, `${theme}.png`);
81
81
  }
82
- async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006") {
82
+ function screenshotExists(storyId, theme) {
83
+ const screenshotPath = getScreenshotPath(storyId, theme);
84
+ return fs.existsSync(screenshotPath);
85
+ }
86
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
83
87
  const browser2 = await getBrowser();
84
88
  const context = await browser2.newContext({
85
89
  viewport: { width, height },
@@ -88,10 +92,10 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
88
92
  const page = await context.newPage();
89
93
  try {
90
94
  const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
91
- await page.goto(url, { timeout: 15e3 });
92
- await page.waitForLoadState("domcontentloaded");
93
- await page.waitForLoadState("load");
94
- await page.waitForLoadState("networkidle");
95
+ await page.goto(url, { timeout: timeoutMs });
96
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
97
+ await page.waitForLoadState("load", { timeout: timeoutMs });
98
+ await page.waitForLoadState("networkidle", { timeout: timeoutMs });
95
99
  await page.evaluate(() => document.fonts.ready);
96
100
  await page.evaluate(async () => {
97
101
  const images = document.querySelectorAll("img");
@@ -154,7 +158,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
154
158
  await context.close();
155
159
  }
156
160
  }
157
- async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006") {
161
+ async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
158
162
  try {
159
163
  ensureCacheDirectories();
160
164
  const storyDir = path.join(SCREENSHOTS_DIR, storyId);
@@ -167,7 +171,8 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
167
171
  theme,
168
172
  VIEWPORT_WIDTH,
169
173
  VIEWPORT_HEIGHT,
170
- storybookUrl
174
+ storybookUrl,
175
+ timeoutMs
171
176
  );
172
177
  fs.writeFileSync(screenshotPath, buffer);
173
178
  return { path: screenshotPath, boundingBox };
@@ -178,9 +183,12 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
178
183
  }
179
184
 
180
185
  // src/screenshot-service/screenshot-service.ts
181
- async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006") {
182
- console.log(`Generating screenshots for ${stories.length} stories...`);
183
- const BATCH_SIZE = 10;
186
+ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
187
+ const displayTotal = total ?? offset + stories.length;
188
+ console.log(
189
+ `Generating screenshots for ${stories.length} stories (concurrency: ${concurrency})...`
190
+ );
191
+ const BATCH_SIZE = concurrency;
184
192
  const batches = [];
185
193
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
186
194
  batches.push(stories.slice(i, i + BATCH_SIZE));
@@ -189,17 +197,24 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
189
197
  for (const batch of batches) {
190
198
  await Promise.all(
191
199
  batch.map(async (story) => {
200
+ if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
201
+ completed++;
202
+ const absoluteIndex2 = offset + completed;
203
+ console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
204
+ return;
205
+ }
192
206
  const [lightResult, darkResult] = await Promise.all([
193
- generateScreenshot(story.id, "light", storybookUrl),
194
- generateScreenshot(story.id, "dark", storybookUrl)
207
+ generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
208
+ generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
195
209
  ]);
196
210
  if (lightResult && darkResult) {
197
211
  const fileHash = computeFileHash(story.importPath);
198
212
  updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
199
213
  }
200
214
  completed++;
215
+ const absoluteIndex = offset + completed;
201
216
  console.log(
202
- `[${completed}/${stories.length}] Generated screenshots for ${story.id}`
217
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
203
218
  );
204
219
  })
205
220
  );
@@ -277,7 +292,9 @@ async function startStorybook(command2, args, storybookDir) {
277
292
  const storybookProcess = spawn(command2, args, {
278
293
  cwd: storybookDir,
279
294
  stdio: "pipe",
280
- shell: true
295
+ shell: true,
296
+ detached: true
297
+ // Create new process group so we can kill all children
281
298
  });
282
299
  return new Promise((resolve3, reject) => {
283
300
  let started = false;
@@ -342,11 +359,20 @@ async function generateScreenshots(options = {}) {
342
359
  storybookProcess = await startStorybook(command2, args, storybookDir);
343
360
  weStartedStorybook = true;
344
361
  }
345
- const stories = await fetchStoryIndex(url);
346
- console.log(`Found ${stories.length} stories`);
362
+ const allStories = await fetchStoryIndex(url);
363
+ const total = allStories.length;
364
+ console.log(`Found ${total} stories`);
365
+ const skipN = options.skip ?? 0;
366
+ const end = options.limit ? skipN + options.limit : total;
367
+ const stories = allStories.slice(skipN, end);
368
+ if (skipN > 0 || options.limit) {
369
+ console.log(
370
+ `Processing stories ${skipN + 1}\u2013${skipN + stories.length} of ${total}`
371
+ );
372
+ }
347
373
  const firstStory = stories[0];
348
374
  if (!firstStory) {
349
- throw new Error("No stories found");
375
+ throw new Error("No stories found in selected range");
350
376
  }
351
377
  await warmupStorybook(url, firstStory.id);
352
378
  await generateAllScreenshots(
@@ -354,14 +380,32 @@ async function generateScreenshots(options = {}) {
354
380
  id: story.id,
355
381
  importPath: story.importPath
356
382
  })),
357
- url
383
+ url,
384
+ options.concurrency,
385
+ skipN,
386
+ total,
387
+ options.skipExisting,
388
+ options.timeout
358
389
  );
359
390
  console.log("\u2705 Screenshot generation complete!");
391
+ } catch (error) {
392
+ console.error("\u274C Screenshot generation failed:", error);
393
+ process.exit(1);
360
394
  } finally {
361
- if (storybookProcess && weStartedStorybook) {
395
+ await closeBrowser();
396
+ if (storybookProcess && weStartedStorybook && storybookProcess.pid) {
362
397
  console.log("\u{1F6D1} Stopping Storybook...");
363
- storybookProcess.kill();
398
+ try {
399
+ process.kill(-storybookProcess.pid, "SIGKILL");
400
+ } catch {
401
+ try {
402
+ storybookProcess.kill("SIGKILL");
403
+ } catch {
404
+ }
405
+ }
364
406
  }
407
+ console.log("\u2705 Cleanup complete, exiting...");
408
+ process.exit(0);
365
409
  }
366
410
  }
367
411
 
@@ -370,13 +414,25 @@ var generateScreenshotsCommand = command({
370
414
  name: "generate-screenshots",
371
415
  desc: "Generate screenshots for all Storybook stories",
372
416
  options: {
373
- storybookDir: string().alias("d").desc("Directory containing Storybook (defaults to current directory)")
417
+ storybookDir: string().alias("d").desc("Directory containing Storybook (defaults to current directory)"),
418
+ limit: string().alias("l").desc("Limit number of stories to process"),
419
+ skip: string().alias("s").desc("Skip first N stories"),
420
+ concurrency: string().alias("c").desc("Number of concurrent screenshots (default: 10)"),
421
+ skipExisting: boolean().alias("e").desc("Skip stories that already have screenshots on disk"),
422
+ timeout: string().alias("t").desc("Timeout per screenshot in ms (default: 30000)")
374
423
  },
375
424
  handler: async (opts) => {
376
- await generateScreenshots({ storybookDir: opts.storybookDir });
425
+ await generateScreenshots({
426
+ storybookDir: opts.storybookDir,
427
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
428
+ skip: opts.skip ? Number.parseInt(opts.skip, 10) : void 0,
429
+ concurrency: opts.concurrency ? Number.parseInt(opts.concurrency, 10) : void 0,
430
+ skipExisting: opts.skipExisting ?? false,
431
+ timeout: opts.timeout ? Number.parseInt(opts.timeout, 10) : void 0
432
+ });
377
433
  }
378
434
  });
379
435
  run([generateScreenshotsCommand], {
380
436
  name: "onlook-storybook",
381
- description: "Storybook plugin for Onlook - generate screenshots and more"
437
+ description: "Storybook plugin for Onbook - generate screenshots and more"
382
438
  });
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import fs, { existsSync } from 'fs';
1
+ import fs4, { existsSync } from 'fs';
2
2
  import path2, { dirname, join, relative } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import generateModule from '@babel/generator';
@@ -78,30 +78,30 @@ var MIN_COMPONENT_HEIGHT = 280;
78
78
 
79
79
  // src/utils/fileSystem/fileSystem.ts
80
80
  function ensureCacheDirectories() {
81
- if (!fs.existsSync(CACHE_DIR)) {
82
- fs.mkdirSync(CACHE_DIR, { recursive: true });
81
+ if (!fs4.existsSync(CACHE_DIR)) {
82
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
83
83
  }
84
- if (!fs.existsSync(SCREENSHOTS_DIR)) {
85
- fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
84
+ if (!fs4.existsSync(SCREENSHOTS_DIR)) {
85
+ fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
86
86
  }
87
87
  }
88
88
  function computeFileHash(filePath) {
89
- if (!fs.existsSync(filePath)) {
89
+ if (!fs4.existsSync(filePath)) {
90
90
  return "";
91
91
  }
92
- const content = fs.readFileSync(filePath, "utf-8");
92
+ const content = fs4.readFileSync(filePath, "utf-8");
93
93
  return crypto.createHash("sha256").update(content).digest("hex");
94
94
  }
95
95
  function loadManifest() {
96
- if (fs.existsSync(MANIFEST_PATH)) {
97
- const content = fs.readFileSync(MANIFEST_PATH, "utf-8");
96
+ if (fs4.existsSync(MANIFEST_PATH)) {
97
+ const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
98
98
  return JSON.parse(content);
99
99
  }
100
100
  return { stories: {} };
101
101
  }
102
102
  function saveManifest(manifest) {
103
103
  ensureCacheDirectories();
104
- fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
104
+ fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
105
105
  }
106
106
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
107
107
  const manifest = loadManifest();
@@ -130,7 +130,7 @@ function getScreenshotPath(storyId, theme) {
130
130
  const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
131
131
  return path2.join(storyDir, `${theme}.png`);
132
132
  }
133
- async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006") {
133
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
134
134
  const browser2 = await getBrowser();
135
135
  const context = await browser2.newContext({
136
136
  viewport: { width, height },
@@ -139,10 +139,10 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
139
139
  const page = await context.newPage();
140
140
  try {
141
141
  const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
142
- await page.goto(url, { timeout: 15e3 });
143
- await page.waitForLoadState("domcontentloaded");
144
- await page.waitForLoadState("load");
145
- await page.waitForLoadState("networkidle");
142
+ await page.goto(url, { timeout: timeoutMs });
143
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
144
+ await page.waitForLoadState("load", { timeout: timeoutMs });
145
+ await page.waitForLoadState("networkidle", { timeout: timeoutMs });
146
146
  await page.evaluate(() => document.fonts.ready);
147
147
  await page.evaluate(async () => {
148
148
  const images = document.querySelectorAll("img");
@@ -205,12 +205,12 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
205
205
  await context.close();
206
206
  }
207
207
  }
208
- async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006") {
208
+ async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
209
209
  try {
210
210
  ensureCacheDirectories();
211
211
  const storyDir = path2.join(SCREENSHOTS_DIR, storyId);
212
- if (!fs.existsSync(storyDir)) {
213
- fs.mkdirSync(storyDir, { recursive: true });
212
+ if (!fs4.existsSync(storyDir)) {
213
+ fs4.mkdirSync(storyDir, { recursive: true });
214
214
  }
215
215
  const screenshotPath = getScreenshotPath(storyId, theme);
216
216
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -218,9 +218,10 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
218
218
  theme,
219
219
  VIEWPORT_WIDTH,
220
220
  VIEWPORT_HEIGHT,
221
- storybookUrl
221
+ storybookUrl,
222
+ timeoutMs
222
223
  );
223
- fs.writeFileSync(screenshotPath, buffer);
224
+ fs4.writeFileSync(screenshotPath, buffer);
224
225
  return { path: screenshotPath, boundingBox };
225
226
  } catch (error) {
226
227
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -336,20 +337,47 @@ var DEFAULT_ALLOWED_ORIGINS = [
336
337
  ];
337
338
  var serveMetadataAndScreenshots = (req, res, next) => {
338
339
  if (req.url === "/onbook-index.json") {
340
+ console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
339
341
  const manifestPath = path2.join(process.cwd(), ".storybook-cache", "manifest.json");
340
- fetch("http://localhost:6006/index.json").then((response) => response.json()).then((indexData) => {
341
- const manifest = fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, "utf-8")) : { stories: {} };
342
+ const cacheBuster = Date.now();
343
+ console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
344
+ fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
345
+ cache: "no-store",
346
+ headers: {
347
+ "Cache-Control": "no-cache",
348
+ Pragma: "no-cache"
349
+ }
350
+ }).then((response) => {
351
+ console.log("[STORYBOOK_PLUGIN] Storybook index fetch response", {
352
+ status: response.status,
353
+ ok: response.ok,
354
+ statusText: response.statusText
355
+ });
356
+ return response.json();
357
+ }).then((indexData) => {
358
+ const manifest = fs4.existsSync(manifestPath) ? JSON.parse(fs4.readFileSync(manifestPath, "utf-8")) : { stories: {} };
342
359
  const defaultBoundingBox = { width: 1920, height: 1080 };
343
360
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
344
361
  const manifestEntry = manifest.stories?.[storyId];
345
362
  entry.boundingBox = manifestEntry?.boundingBox || defaultBoundingBox;
346
363
  }
347
364
  indexData.meta = { storybookLocation, repoRoot };
365
+ console.log("[STORYBOOK_PLUGIN] Successfully enriched index.json", {
366
+ entryCount: Object.keys(indexData.entries || {}).length,
367
+ hasMetadata: !!indexData.meta
368
+ });
348
369
  res.setHeader("Content-Type", "application/json");
349
370
  res.setHeader("Access-Control-Allow-Origin", "*");
371
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
372
+ res.setHeader("Pragma", "no-cache");
373
+ res.setHeader("Expires", "0");
350
374
  res.end(JSON.stringify(indexData));
351
375
  }).catch((error) => {
352
- console.error("Failed to fetch/extend index.json:", error);
376
+ console.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
377
+ error: error instanceof Error ? error.message : String(error),
378
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
379
+ stack: error instanceof Error ? error.stack : void 0
380
+ });
353
381
  res.statusCode = 500;
354
382
  res.setHeader("Content-Type", "application/json");
355
383
  res.end(
@@ -400,25 +428,71 @@ var serveMetadataAndScreenshots = (req, res, next) => {
400
428
  ".storybook-cache",
401
429
  req.url.replace("/screenshots/", "screenshots/")
402
430
  );
403
- if (fs.existsSync(screenshotPath)) {
431
+ const urlParts = req.url.replace("/screenshots/", "").split("/");
432
+ const storyId = urlParts[0];
433
+ const themeFile = urlParts[1];
434
+ const theme = themeFile?.replace(".png", "");
435
+ if (fs4.existsSync(screenshotPath)) {
404
436
  res.setHeader("Content-Type", "image/png");
405
437
  res.setHeader("Access-Control-Allow-Origin", "*");
406
438
  res.setHeader("Cache-Control", "public, max-age=3600");
407
- fs.createReadStream(screenshotPath).pipe(res);
408
- } else {
409
- res.statusCode = 404;
410
- res.end("Screenshot not found");
439
+ fs4.createReadStream(screenshotPath).pipe(res);
440
+ return;
411
441
  }
442
+ if (storyId && theme && (theme === "light" || theme === "dark")) {
443
+ console.log(
444
+ `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
445
+ );
446
+ captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
447
+ const storyDir = path2.join(
448
+ process.cwd(),
449
+ ".storybook-cache",
450
+ "screenshots",
451
+ storyId
452
+ );
453
+ if (!fs4.existsSync(storyDir)) {
454
+ fs4.mkdirSync(storyDir, { recursive: true });
455
+ }
456
+ fs4.writeFileSync(screenshotPath, buffer);
457
+ res.setHeader("Content-Type", "image/png");
458
+ res.setHeader("Access-Control-Allow-Origin", "*");
459
+ res.setHeader("Cache-Control", "public, max-age=3600");
460
+ res.end(buffer);
461
+ }).catch((error) => {
462
+ console.error(
463
+ `[STORYBOOK_PLUGIN] Failed to generate screenshot: ${storyId}/${theme}`,
464
+ error
465
+ );
466
+ res.statusCode = 500;
467
+ res.setHeader("Content-Type", "application/json");
468
+ res.end(
469
+ JSON.stringify({
470
+ error: "Failed to generate screenshot",
471
+ details: String(error)
472
+ })
473
+ );
474
+ });
475
+ return;
476
+ }
477
+ res.statusCode = 404;
478
+ res.end("Screenshot not found");
412
479
  return;
413
480
  }
414
481
  next();
415
482
  };
416
483
  function storybookOnlookPlugin(options = {}) {
417
484
  if (process.env.CHROMATIC || process.env.CI) {
485
+ console.log("[STORYBOOK_PLUGIN] Disabled in CI/Chromatic environment");
418
486
  return [];
419
487
  }
420
488
  const port = options.port ?? 6006;
421
489
  const allowedOrigins = [...DEFAULT_ALLOWED_ORIGINS, ...options.allowedOrigins ?? []];
490
+ console.log("[STORYBOOK_PLUGIN] Plugin initialized", {
491
+ port,
492
+ allowedOrigins,
493
+ storybookLocation,
494
+ repoRoot
495
+ });
422
496
  const mainPlugin = {
423
497
  name: "storybook-onlook-plugin",
424
498
  config() {
@@ -440,9 +514,17 @@ function storybookOnlookPlugin(options = {}) {
440
514
  };
441
515
  },
442
516
  configureServer(server) {
517
+ console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
443
518
  server.middlewares.use(serveMetadataAndScreenshots);
519
+ server.httpServer?.once("listening", () => {
520
+ console.log("[STORYBOOK_PLUGIN] Server is listening", {
521
+ port: server.config.server.port,
522
+ host: server.config.server.host
523
+ });
524
+ });
444
525
  },
445
526
  configurePreviewServer(server) {
527
+ console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
446
528
  server.middlewares.use(serveMetadataAndScreenshots);
447
529
  },
448
530
  handleHotUpdate: handleStoryFileChange
@@ -3,11 +3,14 @@ import 'playwright';
3
3
 
4
4
  /**
5
5
  * Generate screenshots for all stories (parallelized for speed)
6
+ *
7
+ * @param offset - Absolute index offset for logging (e.g. skip=200 → offset=200)
8
+ * @param total - Total number of stories across all runs (for [n/total] logging)
6
9
  */
7
10
  declare function generateAllScreenshots(stories: Array<{
8
11
  id: string;
9
12
  importPath: string;
10
- }>, storybookUrl?: string): Promise<void>;
13
+ }>, storybookUrl?: string, concurrency?: number, offset?: number, total?: number, skipExisting?: boolean, timeoutMs?: number): Promise<void>;
11
14
 
12
15
  interface BoundingBox {
13
16
  width: number;
@@ -42,6 +45,6 @@ declare function screenshotExists(storyId: string, theme: 'light' | 'dark'): boo
42
45
  /**
43
46
  * Generate a screenshot for a story and save to disk
44
47
  */
45
- declare function generateScreenshot(storyId: string, theme: 'light' | 'dark', storybookUrl?: string): Promise<GenerateScreenshotResult | null>;
48
+ declare function generateScreenshot(storyId: string, theme: 'light' | 'dark', storybookUrl?: string, timeoutMs?: number): Promise<GenerateScreenshotResult | null>;
46
49
 
47
50
  export { type Manifest, type ScreenshotMetadata, generateAllScreenshots, generateScreenshot, getScreenshotPath, screenshotExists };
@@ -81,7 +81,7 @@ function screenshotExists(storyId, theme) {
81
81
  const screenshotPath = getScreenshotPath(storyId, theme);
82
82
  return fs.existsSync(screenshotPath);
83
83
  }
84
- async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006") {
84
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
85
85
  const browser2 = await getBrowser();
86
86
  const context = await browser2.newContext({
87
87
  viewport: { width, height },
@@ -90,10 +90,10 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
90
90
  const page = await context.newPage();
91
91
  try {
92
92
  const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
93
- await page.goto(url, { timeout: 15e3 });
94
- await page.waitForLoadState("domcontentloaded");
95
- await page.waitForLoadState("load");
96
- await page.waitForLoadState("networkidle");
93
+ await page.goto(url, { timeout: timeoutMs });
94
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
95
+ await page.waitForLoadState("load", { timeout: timeoutMs });
96
+ await page.waitForLoadState("networkidle", { timeout: timeoutMs });
97
97
  await page.evaluate(() => document.fonts.ready);
98
98
  await page.evaluate(async () => {
99
99
  const images = document.querySelectorAll("img");
@@ -156,7 +156,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
156
156
  await context.close();
157
157
  }
158
158
  }
159
- async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006") {
159
+ async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
160
160
  try {
161
161
  ensureCacheDirectories();
162
162
  const storyDir = path.join(SCREENSHOTS_DIR, storyId);
@@ -169,7 +169,8 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
169
169
  theme,
170
170
  VIEWPORT_WIDTH,
171
171
  VIEWPORT_HEIGHT,
172
- storybookUrl
172
+ storybookUrl,
173
+ timeoutMs
173
174
  );
174
175
  fs.writeFileSync(screenshotPath, buffer);
175
176
  return { path: screenshotPath, boundingBox };
@@ -180,9 +181,12 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
180
181
  }
181
182
 
182
183
  // src/screenshot-service/screenshot-service.ts
183
- async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006") {
184
- console.log(`Generating screenshots for ${stories.length} stories...`);
185
- const BATCH_SIZE = 10;
184
+ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
185
+ const displayTotal = total ?? offset + stories.length;
186
+ console.log(
187
+ `Generating screenshots for ${stories.length} stories (concurrency: ${concurrency})...`
188
+ );
189
+ const BATCH_SIZE = concurrency;
186
190
  const batches = [];
187
191
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
188
192
  batches.push(stories.slice(i, i + BATCH_SIZE));
@@ -191,17 +195,24 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
191
195
  for (const batch of batches) {
192
196
  await Promise.all(
193
197
  batch.map(async (story) => {
198
+ if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
199
+ completed++;
200
+ const absoluteIndex2 = offset + completed;
201
+ console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
202
+ return;
203
+ }
194
204
  const [lightResult, darkResult] = await Promise.all([
195
- generateScreenshot(story.id, "light", storybookUrl),
196
- generateScreenshot(story.id, "dark", storybookUrl)
205
+ generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
206
+ generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
197
207
  ]);
198
208
  if (lightResult && darkResult) {
199
209
  const fileHash = computeFileHash(story.importPath);
200
210
  updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
201
211
  }
202
212
  completed++;
213
+ const absoluteIndex = offset + completed;
203
214
  console.log(
204
- `[${completed}/${stories.length}] Generated screenshots for ${story.id}`
215
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
205
216
  );
206
217
  })
207
218
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "onlook-storybook": "./dist/cli/index.js"
@@ -29,7 +29,7 @@
29
29
  "typecheck": "tsc --noEmit",
30
30
  "prepublishOnly": "bun run build && bun scripts/prepublish.ts",
31
31
  "postpublish": "bun scripts/postpublish.ts",
32
- "publish-pkg": "npm publish --access public"
32
+ "publish-pkg": "npm publish"
33
33
  },
34
34
  "dependencies": {
35
35
  "@babel/generator": "^7.26.9",
@@ -49,6 +49,9 @@
49
49
  "typescript": "5.8.3",
50
50
  "vite": "^6.3.5"
51
51
  },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
52
55
  "peerDependencies": {
53
56
  "vite": "^5.0.0 || ^6.0.0"
54
57
  }