@onlook/storybook-plugin 0.3.1 → 0.3.2-beta.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
@@ -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;
@@ -183,6 +190,33 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
183
190
  }
184
191
 
185
192
  // src/screenshot-service/screenshot-service.ts
193
+ var StoryTimeoutError = class extends Error {
194
+ constructor(storyId, timeoutMs) {
195
+ super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
196
+ this.storyId = storyId;
197
+ this.timeoutMs = timeoutMs;
198
+ this.name = "StoryTimeoutError";
199
+ }
200
+ };
201
+ async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
202
+ let timer;
203
+ try {
204
+ return await Promise.race([
205
+ Promise.all([
206
+ generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
207
+ generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
208
+ ]),
209
+ new Promise((_, reject) => {
210
+ timer = setTimeout(
211
+ () => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
212
+ storyTimeoutMs
213
+ );
214
+ })
215
+ ]);
216
+ } finally {
217
+ if (timer) clearTimeout(timer);
218
+ }
219
+ }
186
220
  async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
187
221
  const displayTotal = total ?? offset + stories.length;
188
222
  console.log(
@@ -193,6 +227,7 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
193
227
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
194
228
  batches.push(stories.slice(i, i + BATCH_SIZE));
195
229
  }
230
+ const storyTimeout = timeoutMs * 2 + 1e4;
196
231
  let completed = 0;
197
232
  for (const batch of batches) {
198
233
  await Promise.all(
@@ -203,19 +238,64 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
203
238
  console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
204
239
  return;
205
240
  }
206
- const [lightResult, darkResult] = await Promise.all([
207
- generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
208
- generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
209
- ]);
241
+ let lightResult = null;
242
+ let darkResult = null;
243
+ let lastError;
244
+ const startedAt = Date.now();
245
+ let attempts = 0;
246
+ const maxAttempts = 2;
247
+ while (attempts < maxAttempts) {
248
+ attempts++;
249
+ try {
250
+ [lightResult, darkResult] = await captureBothThemes(
251
+ story.id,
252
+ storybookUrl,
253
+ timeoutMs,
254
+ storyTimeout
255
+ );
256
+ lastError = void 0;
257
+ break;
258
+ } catch (error) {
259
+ lastError = error;
260
+ if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
261
+ break;
262
+ }
263
+ console.warn(
264
+ `[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
265
+ );
266
+ }
267
+ }
268
+ completed++;
269
+ const absoluteIndex = offset + completed;
270
+ const durationMs = Date.now() - startedAt;
210
271
  if (lightResult && darkResult) {
211
272
  const fileHash = computeFileHash(story.importPath);
212
273
  updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
274
+ console.log(
275
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
276
+ { storyId: story.id, attempts, durationMs, outcome: "ok" }
277
+ );
278
+ } else if (lastError) {
279
+ console.error(
280
+ `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
281
+ lastError instanceof Error ? lastError.message : lastError,
282
+ {
283
+ storyId: story.id,
284
+ attempts,
285
+ durationMs,
286
+ outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
287
+ }
288
+ );
289
+ } else {
290
+ console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
291
+ storyId: story.id,
292
+ attempts,
293
+ durationMs,
294
+ outcome: "partial",
295
+ hasLight: !!lightResult,
296
+ hasDark: !!darkResult
297
+ });
213
298
  }
214
- completed++;
215
- const absoluteIndex = offset + completed;
216
- console.log(
217
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
218
- );
219
299
  })
220
300
  );
221
301
  }
@@ -260,10 +340,27 @@ function getStorybookCommand(storybookDir) {
260
340
  console.log(
261
341
  `\u{1F4E6} Detected package manager: ${pm}${repoRoot ? ` (from ${repoRoot})` : ""}`
262
342
  );
263
- const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
343
+ let extraArgs = [];
344
+ try {
345
+ const pkgPath = resolve(storybookDir, "package.json");
346
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
347
+ const script = pkg.scripts?.storybook ?? "";
348
+ const hasPort = script.includes("-p ") || script.includes("--port");
349
+ const hasNoOpen = script.includes("--no-open");
350
+ if (!hasPort || !hasNoOpen) {
351
+ const needsSeparator = pm === "npm" || pm === "pnpm";
352
+ const flags = [];
353
+ if (!hasPort) flags.push("-p", "6006");
354
+ if (!hasNoOpen) flags.push("--no-open");
355
+ extraArgs = needsSeparator && flags.length > 0 ? ["--", ...flags] : flags;
356
+ }
357
+ } catch {
358
+ const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
359
+ extraArgs = [...separator, "-p", "6006", "--no-open"];
360
+ }
264
361
  return {
265
362
  command: pm,
266
- args: ["run", "storybook", ...separator, "-p", "6006", "--no-open"]
363
+ args: ["run", "storybook", ...extraArgs]
267
364
  };
268
365
  }
269
366
  async function isStorybookRunning(url) {
@@ -276,14 +373,38 @@ async function isStorybookRunning(url) {
276
373
  return false;
277
374
  }
278
375
  }
279
- async function fetchStoryIndex(url) {
376
+ async function waitForStorybookReady(url, timeoutMs = 12e4) {
377
+ const start = Date.now();
378
+ const pollInterval = 2e3;
379
+ while (Date.now() - start < timeoutMs) {
380
+ if (await isStorybookRunning(url)) return;
381
+ await new Promise((r) => setTimeout(r, pollInterval));
382
+ }
383
+ throw new Error(`Storybook did not become ready within ${timeoutMs / 1e3}s`);
384
+ }
385
+ async function fetchStoryIndex(url, retries = 3) {
280
386
  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}`);
387
+ for (let attempt = 1; attempt <= retries; attempt++) {
388
+ try {
389
+ const response = await fetch(indexUrl, {
390
+ signal: AbortSignal.timeout(1e4)
391
+ });
392
+ if (!response.ok) {
393
+ throw new Error(
394
+ `Failed to fetch story index: ${response.status} ${response.statusText}`
395
+ );
396
+ }
397
+ const data = await response.json();
398
+ return Object.values(data.entries);
399
+ } catch (error) {
400
+ if (attempt === retries) throw error;
401
+ console.log(
402
+ `\u26A0\uFE0F Fetch story index failed (attempt ${attempt}/${retries}), retrying in 3s...`
403
+ );
404
+ await new Promise((r) => setTimeout(r, 3e3));
405
+ }
284
406
  }
285
- const data = await response.json();
286
- return Object.values(data.entries);
407
+ throw new Error("Unreachable");
287
408
  }
288
409
  async function startStorybook(command2, args, storybookDir) {
289
410
  console.log(
@@ -301,12 +422,12 @@ async function startStorybook(command2, args, storybookDir) {
301
422
  const timeout = setTimeout(() => {
302
423
  if (!started) {
303
424
  storybookProcess.kill();
304
- reject(new Error("Storybook failed to start within 60 seconds"));
425
+ reject(new Error("Storybook failed to start within 180 seconds"));
305
426
  }
306
- }, 6e4);
427
+ }, 18e4);
307
428
  storybookProcess.stdout?.on("data", (data) => {
308
429
  const output = data.toString();
309
- if (output.includes("Local:") || output.includes("localhost:")) {
430
+ if (output.includes("Local:") || output.includes("localhost:") || output.includes("local:")) {
310
431
  if (!started) {
311
432
  started = true;
312
433
  clearTimeout(timeout);
@@ -335,7 +456,7 @@ async function warmupStorybook(url, firstStoryId) {
335
456
  try {
336
457
  const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
337
458
  await page.goto(warmupUrl, { timeout: 15e3 });
338
- await page.waitForLoadState("networkidle");
459
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 });
339
460
  console.log("\u2705 Storybook warmed up");
340
461
  } catch {
341
462
  console.log("\u26A0\uFE0F Warmup had issues, proceeding anyway");
@@ -358,6 +479,8 @@ async function generateScreenshots(options = {}) {
358
479
  const { command: command2, args } = getStorybookCommand(storybookDir);
359
480
  storybookProcess = await startStorybook(command2, args, storybookDir);
360
481
  weStartedStorybook = true;
482
+ console.log("\u23F3 Waiting for Storybook to finish compiling...");
483
+ await waitForStorybookReady(url);
361
484
  }
362
485
  const allStories = await fetchStoryIndex(url);
363
486
  const total = allStories.length;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  import { PluginOption } from 'vite';
2
+ import { Indexer } from 'storybook/internal/types';
2
3
 
3
4
  type OnlookPluginOptions = {
4
5
  /** Storybook port (default: 6006) */
5
6
  port?: number;
6
7
  /** Additional allowed origins for CORS (merged with defaults) */
7
8
  allowedOrigins?: string[];
9
+ /**
10
+ * Set when this plugin runs inside an E2B sandbox. Enables the
11
+ * WSS:443 HMR override required for Vite's HMR client to talk back
12
+ * through E2B's HTTPS port-proxy. Leave false (default) for local
13
+ * `bun dev:storybook` so HMR uses Vite defaults and actually fires.
14
+ */
15
+ e2b?: boolean;
8
16
  };
9
17
  declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
10
18
 
11
- export { type OnlookPluginOptions, storybookOnlookPlugin };
19
+ declare const tolerantCsfIndexer: Indexer;
20
+
21
+ export { type OnlookPluginOptions, storybookOnlookPlugin, tolerantCsfIndexer };