@onlook/storybook-plugin 0.3.2 → 0.3.3

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
@@ -44,7 +44,7 @@ Configures Vite HMR for E2B sandboxes (WSS protocol, port 443 routing).
44
44
 
45
45
  ### CORS
46
46
 
47
- Pre-configured for `https://app.onlook.ai`, `localhost:3000`, and `localhost:6006`.
47
+ Pre-configured for `https://app.onlook.com`, `localhost:3000`, and `localhost:6006`.
48
48
 
49
49
  ## Options
50
50
 
package/dist/cli/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { command, string, boolean, run } from '@drizzle-team/brocli';
3
3
  import { spawn } from 'child_process';
4
+ import fs, { readFileSync, existsSync } from 'fs';
4
5
  import path, { resolve, join, dirname } from 'path';
5
6
  import crypto from 'crypto';
6
- import fs, { existsSync } from 'fs';
7
7
  import { chromium } from 'playwright';
8
8
 
9
9
  var CACHE_DIR = path.join(process.cwd(), ".storybook-cache");
@@ -95,20 +95,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
95
95
  await page.goto(url, { timeout: timeoutMs });
96
96
  await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
97
97
  await page.waitForLoadState("load", { timeout: timeoutMs });
98
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
98
+ try {
99
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
100
+ } catch {
101
+ }
99
102
  await page.evaluate(() => document.fonts.ready);
100
- await page.evaluate(async () => {
101
- const images = document.querySelectorAll("img");
102
- await Promise.all(
103
- Array.from(images).map((img) => {
104
- if (img.complete) return Promise.resolve();
105
- return new Promise((resolve3) => {
106
- img.addEventListener("load", resolve3);
107
- img.addEventListener("error", resolve3);
108
- });
109
- })
110
- );
111
- });
103
+ try {
104
+ await page.evaluate(async () => {
105
+ const images = document.querySelectorAll("img");
106
+ await Promise.all(
107
+ Array.from(images).map((img) => {
108
+ if (img.complete) return Promise.resolve();
109
+ return new Promise((resolve3) => {
110
+ img.addEventListener("load", resolve3);
111
+ img.addEventListener("error", resolve3);
112
+ setTimeout(resolve3, 3e3);
113
+ });
114
+ })
115
+ );
116
+ });
117
+ } catch {
118
+ }
112
119
  const contentBounds = await page.evaluate(() => {
113
120
  const root = document.querySelector("#storybook-root");
114
121
  if (!root) return null;
@@ -199,23 +206,48 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
199
206
  batch.map(async (story) => {
200
207
  if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
201
208
  completed++;
202
- const absoluteIndex2 = offset + completed;
203
- console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
209
+ const absoluteIndex = offset + completed;
210
+ console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
204
211
  return;
205
212
  }
206
- const [lightResult, darkResult] = await Promise.all([
207
- generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
208
- generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
209
- ]);
210
- if (lightResult && darkResult) {
211
- const fileHash = computeFileHash(story.importPath);
212
- updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
213
+ const storyTimeout = timeoutMs * 2 + 1e4;
214
+ let timer;
215
+ try {
216
+ const result = await Promise.race([
217
+ Promise.all([
218
+ generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
219
+ generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
220
+ ]),
221
+ new Promise((_, reject) => {
222
+ timer = setTimeout(
223
+ () => reject(
224
+ new Error(
225
+ `Story ${story.id} timed out after ${storyTimeout / 1e3}s`
226
+ )
227
+ ),
228
+ storyTimeout
229
+ );
230
+ })
231
+ ]);
232
+ clearTimeout(timer);
233
+ const [lightResult, darkResult] = result;
234
+ if (lightResult && darkResult) {
235
+ const fileHash = computeFileHash(story.importPath);
236
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
237
+ }
238
+ completed++;
239
+ const absoluteIndex = offset + completed;
240
+ console.log(
241
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
242
+ );
243
+ } catch (error) {
244
+ completed++;
245
+ const absoluteIndex = offset + completed;
246
+ console.error(
247
+ `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
248
+ error instanceof Error ? error.message : error
249
+ );
213
250
  }
214
- completed++;
215
- const absoluteIndex = offset + completed;
216
- console.log(
217
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
218
- );
219
251
  })
220
252
  );
221
253
  }
@@ -260,10 +292,27 @@ function getStorybookCommand(storybookDir) {
260
292
  console.log(
261
293
  `\u{1F4E6} Detected package manager: ${pm}${repoRoot ? ` (from ${repoRoot})` : ""}`
262
294
  );
263
- const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
295
+ let extraArgs = [];
296
+ try {
297
+ const pkgPath = resolve(storybookDir, "package.json");
298
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
299
+ const script = pkg.scripts?.storybook ?? "";
300
+ const hasPort = script.includes("-p ") || script.includes("--port");
301
+ const hasNoOpen = script.includes("--no-open");
302
+ if (!hasPort || !hasNoOpen) {
303
+ const needsSeparator = pm === "npm" || pm === "pnpm";
304
+ const flags = [];
305
+ if (!hasPort) flags.push("-p", "6006");
306
+ if (!hasNoOpen) flags.push("--no-open");
307
+ extraArgs = needsSeparator && flags.length > 0 ? ["--", ...flags] : flags;
308
+ }
309
+ } catch {
310
+ const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
311
+ extraArgs = [...separator, "-p", "6006", "--no-open"];
312
+ }
264
313
  return {
265
314
  command: pm,
266
- args: ["run", "storybook", ...separator, "-p", "6006", "--no-open"]
315
+ args: ["run", "storybook", ...extraArgs]
267
316
  };
268
317
  }
269
318
  async function isStorybookRunning(url) {
@@ -276,14 +325,38 @@ async function isStorybookRunning(url) {
276
325
  return false;
277
326
  }
278
327
  }
279
- async function fetchStoryIndex(url) {
328
+ async function waitForStorybookReady(url, timeoutMs = 12e4) {
329
+ const start = Date.now();
330
+ const pollInterval = 2e3;
331
+ while (Date.now() - start < timeoutMs) {
332
+ if (await isStorybookRunning(url)) return;
333
+ await new Promise((r) => setTimeout(r, pollInterval));
334
+ }
335
+ throw new Error(`Storybook did not become ready within ${timeoutMs / 1e3}s`);
336
+ }
337
+ async function fetchStoryIndex(url, retries = 3) {
280
338
  const indexUrl = `${url}/index.json`;
281
- const response = await fetch(indexUrl);
282
- if (!response.ok) {
283
- throw new Error(`Failed to fetch story index: ${response.statusText}`);
339
+ for (let attempt = 1; attempt <= retries; attempt++) {
340
+ try {
341
+ const response = await fetch(indexUrl, {
342
+ signal: AbortSignal.timeout(1e4)
343
+ });
344
+ if (!response.ok) {
345
+ throw new Error(
346
+ `Failed to fetch story index: ${response.status} ${response.statusText}`
347
+ );
348
+ }
349
+ const data = await response.json();
350
+ return Object.values(data.entries);
351
+ } catch (error) {
352
+ if (attempt === retries) throw error;
353
+ console.log(
354
+ `\u26A0\uFE0F Fetch story index failed (attempt ${attempt}/${retries}), retrying in 3s...`
355
+ );
356
+ await new Promise((r) => setTimeout(r, 3e3));
357
+ }
284
358
  }
285
- const data = await response.json();
286
- return Object.values(data.entries);
359
+ throw new Error("Unreachable");
287
360
  }
288
361
  async function startStorybook(command2, args, storybookDir) {
289
362
  console.log(
@@ -301,12 +374,12 @@ async function startStorybook(command2, args, storybookDir) {
301
374
  const timeout = setTimeout(() => {
302
375
  if (!started) {
303
376
  storybookProcess.kill();
304
- reject(new Error("Storybook failed to start within 60 seconds"));
377
+ reject(new Error("Storybook failed to start within 180 seconds"));
305
378
  }
306
- }, 6e4);
379
+ }, 18e4);
307
380
  storybookProcess.stdout?.on("data", (data) => {
308
381
  const output = data.toString();
309
- if (output.includes("Local:") || output.includes("localhost:")) {
382
+ if (output.includes("Local:") || output.includes("localhost:") || output.includes("local:")) {
310
383
  if (!started) {
311
384
  started = true;
312
385
  clearTimeout(timeout);
@@ -335,7 +408,7 @@ async function warmupStorybook(url, firstStoryId) {
335
408
  try {
336
409
  const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
337
410
  await page.goto(warmupUrl, { timeout: 15e3 });
338
- await page.waitForLoadState("networkidle");
411
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 });
339
412
  console.log("\u2705 Storybook warmed up");
340
413
  } catch {
341
414
  console.log("\u26A0\uFE0F Warmup had issues, proceeding anyway");
@@ -358,6 +431,8 @@ async function generateScreenshots(options = {}) {
358
431
  const { command: command2, args } = getStorybookCommand(storybookDir);
359
432
  storybookProcess = await startStorybook(command2, args, storybookDir);
360
433
  weStartedStorybook = true;
434
+ console.log("\u23F3 Waiting for Storybook to finish compiling...");
435
+ await waitForStorybookReady(url);
361
436
  }
362
437
  const allStories = await fetchStoryIndex(url);
363
438
  const total = allStories.length;
package/dist/index.js CHANGED
@@ -142,20 +142,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
142
142
  await page.goto(url, { timeout: timeoutMs });
143
143
  await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
144
144
  await page.waitForLoadState("load", { timeout: timeoutMs });
145
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
145
+ try {
146
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
147
+ } catch {
148
+ }
146
149
  await page.evaluate(() => document.fonts.ready);
147
- await page.evaluate(async () => {
148
- const images = document.querySelectorAll("img");
149
- await Promise.all(
150
- Array.from(images).map((img) => {
151
- if (img.complete) return Promise.resolve();
152
- return new Promise((resolve) => {
153
- img.addEventListener("load", resolve);
154
- img.addEventListener("error", resolve);
155
- });
156
- })
157
- );
158
- });
150
+ try {
151
+ await page.evaluate(async () => {
152
+ const images = document.querySelectorAll("img");
153
+ await Promise.all(
154
+ Array.from(images).map((img) => {
155
+ if (img.complete) return Promise.resolve();
156
+ return new Promise((resolve) => {
157
+ img.addEventListener("load", resolve);
158
+ img.addEventListener("error", resolve);
159
+ setTimeout(resolve, 3e3);
160
+ });
161
+ })
162
+ );
163
+ });
164
+ } catch {
165
+ }
159
166
  const contentBounds = await page.evaluate(() => {
160
167
  const root = document.querySelector("#storybook-root");
161
168
  if (!root) return null;
@@ -331,7 +338,7 @@ var gitRoot = findGitRoot(storybookDir);
331
338
  var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
332
339
  var repoRoot = gitRoot || process.cwd();
333
340
  var DEFAULT_ALLOWED_ORIGINS = [
334
- "https://app.onlook.ai",
341
+ "https://app.onlook.com",
335
342
  "http://localhost:3000",
336
343
  "http://localhost:6006"
337
344
  ];
@@ -496,17 +503,16 @@ function storybookOnlookPlugin(options = {}) {
496
503
  const mainPlugin = {
497
504
  name: "storybook-onlook-plugin",
498
505
  config() {
499
- const isE2B = !!process.env.E2B_SANDBOX;
500
506
  return {
501
507
  server: {
502
- // Only override HMR for E2B sandboxes (wss + port 443)
503
- // Local dev uses Vite's default HMR which works out of the box
504
- ...isE2B && {
505
- hmr: {
506
- protocol: "wss",
507
- clientPort: 443,
508
- port
509
- }
508
+ // E2B sandbox HMR configuration
509
+ hmr: {
510
+ // E2B sandboxes use HTTPS, so we need secure WebSocket
511
+ protocol: "wss",
512
+ // E2B routes through standard HTTPS port 443
513
+ clientPort: 443,
514
+ // The actual Storybook server port inside the sandbox
515
+ port
510
516
  },
511
517
  cors: {
512
518
  origin: allowedOrigins
@@ -528,13 +534,7 @@ function storybookOnlookPlugin(options = {}) {
528
534
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
529
535
  server.middlewares.use(serveMetadataAndScreenshots);
530
536
  },
531
- // Only auto-regenerate screenshots in E2B sandboxes where Onbook
532
- // consumes them. In local dev this blocks HMR with 36 Playwright
533
- // page loads per save. Screenshots are still available on-demand
534
- // via /api/capture-screenshot and the CLI.
535
- ...process.env.E2B_SANDBOX && {
536
- handleHotUpdate: handleStoryFileChange
537
- }
537
+ handleHotUpdate: handleStoryFileChange
538
538
  };
539
539
  return [componentLocPlugin(), mainPlugin];
540
540
  }
@@ -93,20 +93,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
93
93
  await page.goto(url, { timeout: timeoutMs });
94
94
  await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
95
95
  await page.waitForLoadState("load", { timeout: timeoutMs });
96
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
96
+ try {
97
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
98
+ } catch {
99
+ }
97
100
  await page.evaluate(() => document.fonts.ready);
98
- await page.evaluate(async () => {
99
- const images = document.querySelectorAll("img");
100
- await Promise.all(
101
- Array.from(images).map((img) => {
102
- if (img.complete) return Promise.resolve();
103
- return new Promise((resolve) => {
104
- img.addEventListener("load", resolve);
105
- img.addEventListener("error", resolve);
106
- });
107
- })
108
- );
109
- });
101
+ try {
102
+ await page.evaluate(async () => {
103
+ const images = document.querySelectorAll("img");
104
+ await Promise.all(
105
+ Array.from(images).map((img) => {
106
+ if (img.complete) return Promise.resolve();
107
+ return new Promise((resolve) => {
108
+ img.addEventListener("load", resolve);
109
+ img.addEventListener("error", resolve);
110
+ setTimeout(resolve, 3e3);
111
+ });
112
+ })
113
+ );
114
+ });
115
+ } catch {
116
+ }
110
117
  const contentBounds = await page.evaluate(() => {
111
118
  const root = document.querySelector("#storybook-root");
112
119
  if (!root) return null;
@@ -197,23 +204,48 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
197
204
  batch.map(async (story) => {
198
205
  if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
199
206
  completed++;
200
- const absoluteIndex2 = offset + completed;
201
- console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
207
+ const absoluteIndex = offset + completed;
208
+ console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
202
209
  return;
203
210
  }
204
- const [lightResult, darkResult] = await Promise.all([
205
- generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
206
- generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
207
- ]);
208
- if (lightResult && darkResult) {
209
- const fileHash = computeFileHash(story.importPath);
210
- updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
211
+ const storyTimeout = timeoutMs * 2 + 1e4;
212
+ let timer;
213
+ try {
214
+ const result = await Promise.race([
215
+ Promise.all([
216
+ generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
217
+ generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
218
+ ]),
219
+ new Promise((_, reject) => {
220
+ timer = setTimeout(
221
+ () => reject(
222
+ new Error(
223
+ `Story ${story.id} timed out after ${storyTimeout / 1e3}s`
224
+ )
225
+ ),
226
+ storyTimeout
227
+ );
228
+ })
229
+ ]);
230
+ clearTimeout(timer);
231
+ const [lightResult, darkResult] = result;
232
+ if (lightResult && darkResult) {
233
+ const fileHash = computeFileHash(story.importPath);
234
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
235
+ }
236
+ completed++;
237
+ const absoluteIndex = offset + completed;
238
+ console.log(
239
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
240
+ );
241
+ } catch (error) {
242
+ completed++;
243
+ const absoluteIndex = offset + completed;
244
+ console.error(
245
+ `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
246
+ error instanceof Error ? error.message : error
247
+ );
211
248
  }
212
- completed++;
213
- const absoluteIndex = offset + completed;
214
- console.log(
215
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
216
- );
217
249
  })
218
250
  );
219
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "onlook-storybook": "./dist/cli/index.js"