@onlook/storybook-plugin 0.3.1 → 0.3.2-beta.1

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,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import { command, string, boolean, run } from '@drizzle-team/brocli';
3
4
  import { spawn } from 'child_process';
5
+ import fs, { readFileSync, existsSync } from 'fs';
4
6
  import path, { resolve, join, dirname } from 'path';
5
7
  import crypto from 'crypto';
6
- import fs, { existsSync } from 'fs';
7
8
  import { chromium } from 'playwright';
8
9
 
10
+ globalThis.require = createRequire(import.meta.url);
9
11
  var CACHE_DIR = path.join(process.cwd(), ".storybook-cache");
10
12
  var SCREENSHOTS_DIR = path.join(CACHE_DIR, "screenshots");
11
13
  var MANIFEST_PATH = path.join(CACHE_DIR, "manifest.json");
@@ -95,20 +97,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
95
97
  await page.goto(url, { timeout: timeoutMs });
96
98
  await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
97
99
  await page.waitForLoadState("load", { timeout: timeoutMs });
98
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
100
+ try {
101
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
102
+ } catch {
103
+ }
99
104
  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
- });
105
+ try {
106
+ await page.evaluate(async () => {
107
+ const images = document.querySelectorAll("img");
108
+ await Promise.all(
109
+ Array.from(images).map((img) => {
110
+ if (img.complete) return Promise.resolve();
111
+ return new Promise((resolve3) => {
112
+ img.addEventListener("load", resolve3);
113
+ img.addEventListener("error", resolve3);
114
+ setTimeout(resolve3, 3e3);
115
+ });
116
+ })
117
+ );
118
+ });
119
+ } catch {
120
+ }
112
121
  const contentBounds = await page.evaluate(() => {
113
122
  const root = document.querySelector("#storybook-root");
114
123
  if (!root) return null;
@@ -183,6 +192,33 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
183
192
  }
184
193
 
185
194
  // src/screenshot-service/screenshot-service.ts
195
+ var StoryTimeoutError = class extends Error {
196
+ constructor(storyId, timeoutMs) {
197
+ super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
198
+ this.storyId = storyId;
199
+ this.timeoutMs = timeoutMs;
200
+ this.name = "StoryTimeoutError";
201
+ }
202
+ };
203
+ async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
204
+ let timer;
205
+ try {
206
+ return await Promise.race([
207
+ Promise.all([
208
+ generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
209
+ generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
210
+ ]),
211
+ new Promise((_, reject) => {
212
+ timer = setTimeout(
213
+ () => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
214
+ storyTimeoutMs
215
+ );
216
+ })
217
+ ]);
218
+ } finally {
219
+ if (timer) clearTimeout(timer);
220
+ }
221
+ }
186
222
  async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
187
223
  const displayTotal = total ?? offset + stories.length;
188
224
  console.log(
@@ -193,6 +229,7 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
193
229
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
194
230
  batches.push(stories.slice(i, i + BATCH_SIZE));
195
231
  }
232
+ const storyTimeout = timeoutMs * 2 + 1e4;
196
233
  let completed = 0;
197
234
  for (const batch of batches) {
198
235
  await Promise.all(
@@ -203,19 +240,64 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
203
240
  console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
204
241
  return;
205
242
  }
206
- const [lightResult, darkResult] = await Promise.all([
207
- generateScreenshot(story.id, "light", storybookUrl, timeoutMs),
208
- generateScreenshot(story.id, "dark", storybookUrl, timeoutMs)
209
- ]);
243
+ let lightResult = null;
244
+ let darkResult = null;
245
+ let lastError;
246
+ const startedAt = Date.now();
247
+ let attempts = 0;
248
+ const maxAttempts = 2;
249
+ while (attempts < maxAttempts) {
250
+ attempts++;
251
+ try {
252
+ [lightResult, darkResult] = await captureBothThemes(
253
+ story.id,
254
+ storybookUrl,
255
+ timeoutMs,
256
+ storyTimeout
257
+ );
258
+ lastError = void 0;
259
+ break;
260
+ } catch (error) {
261
+ lastError = error;
262
+ if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
263
+ break;
264
+ }
265
+ console.warn(
266
+ `[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
267
+ );
268
+ }
269
+ }
270
+ completed++;
271
+ const absoluteIndex = offset + completed;
272
+ const durationMs = Date.now() - startedAt;
210
273
  if (lightResult && darkResult) {
211
274
  const fileHash = computeFileHash(story.importPath);
212
275
  updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
276
+ console.log(
277
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
278
+ { storyId: story.id, attempts, durationMs, outcome: "ok" }
279
+ );
280
+ } else if (lastError) {
281
+ console.error(
282
+ `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
283
+ lastError instanceof Error ? lastError.message : lastError,
284
+ {
285
+ storyId: story.id,
286
+ attempts,
287
+ durationMs,
288
+ outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
289
+ }
290
+ );
291
+ } else {
292
+ console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
293
+ storyId: story.id,
294
+ attempts,
295
+ durationMs,
296
+ outcome: "partial",
297
+ hasLight: !!lightResult,
298
+ hasDark: !!darkResult
299
+ });
213
300
  }
214
- completed++;
215
- const absoluteIndex = offset + completed;
216
- console.log(
217
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
218
- );
219
301
  })
220
302
  );
221
303
  }
@@ -260,10 +342,27 @@ function getStorybookCommand(storybookDir) {
260
342
  console.log(
261
343
  `\u{1F4E6} Detected package manager: ${pm}${repoRoot ? ` (from ${repoRoot})` : ""}`
262
344
  );
263
- const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
345
+ let extraArgs = [];
346
+ try {
347
+ const pkgPath = resolve(storybookDir, "package.json");
348
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
349
+ const script = pkg.scripts?.storybook ?? "";
350
+ const hasPort = script.includes("-p ") || script.includes("--port");
351
+ const hasNoOpen = script.includes("--no-open");
352
+ if (!hasPort || !hasNoOpen) {
353
+ const needsSeparator = pm === "npm" || pm === "pnpm";
354
+ const flags = [];
355
+ if (!hasPort) flags.push("-p", "6006");
356
+ if (!hasNoOpen) flags.push("--no-open");
357
+ extraArgs = needsSeparator && flags.length > 0 ? ["--", ...flags] : flags;
358
+ }
359
+ } catch {
360
+ const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
361
+ extraArgs = [...separator, "-p", "6006", "--no-open"];
362
+ }
264
363
  return {
265
364
  command: pm,
266
- args: ["run", "storybook", ...separator, "-p", "6006", "--no-open"]
365
+ args: ["run", "storybook", ...extraArgs]
267
366
  };
268
367
  }
269
368
  async function isStorybookRunning(url) {
@@ -276,14 +375,38 @@ async function isStorybookRunning(url) {
276
375
  return false;
277
376
  }
278
377
  }
279
- async function fetchStoryIndex(url) {
378
+ async function waitForStorybookReady(url, timeoutMs = 12e4) {
379
+ const start = Date.now();
380
+ const pollInterval = 2e3;
381
+ while (Date.now() - start < timeoutMs) {
382
+ if (await isStorybookRunning(url)) return;
383
+ await new Promise((r) => setTimeout(r, pollInterval));
384
+ }
385
+ throw new Error(`Storybook did not become ready within ${timeoutMs / 1e3}s`);
386
+ }
387
+ async function fetchStoryIndex(url, retries = 3) {
280
388
  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}`);
389
+ for (let attempt = 1; attempt <= retries; attempt++) {
390
+ try {
391
+ const response = await fetch(indexUrl, {
392
+ signal: AbortSignal.timeout(1e4)
393
+ });
394
+ if (!response.ok) {
395
+ throw new Error(
396
+ `Failed to fetch story index: ${response.status} ${response.statusText}`
397
+ );
398
+ }
399
+ const data = await response.json();
400
+ return Object.values(data.entries);
401
+ } catch (error) {
402
+ if (attempt === retries) throw error;
403
+ console.log(
404
+ `\u26A0\uFE0F Fetch story index failed (attempt ${attempt}/${retries}), retrying in 3s...`
405
+ );
406
+ await new Promise((r) => setTimeout(r, 3e3));
407
+ }
284
408
  }
285
- const data = await response.json();
286
- return Object.values(data.entries);
409
+ throw new Error("Unreachable");
287
410
  }
288
411
  async function startStorybook(command2, args, storybookDir) {
289
412
  console.log(
@@ -301,12 +424,12 @@ async function startStorybook(command2, args, storybookDir) {
301
424
  const timeout = setTimeout(() => {
302
425
  if (!started) {
303
426
  storybookProcess.kill();
304
- reject(new Error("Storybook failed to start within 60 seconds"));
427
+ reject(new Error("Storybook failed to start within 180 seconds"));
305
428
  }
306
- }, 6e4);
429
+ }, 18e4);
307
430
  storybookProcess.stdout?.on("data", (data) => {
308
431
  const output = data.toString();
309
- if (output.includes("Local:") || output.includes("localhost:")) {
432
+ if (output.includes("Local:") || output.includes("localhost:") || output.includes("local:")) {
310
433
  if (!started) {
311
434
  started = true;
312
435
  clearTimeout(timeout);
@@ -335,7 +458,7 @@ async function warmupStorybook(url, firstStoryId) {
335
458
  try {
336
459
  const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
337
460
  await page.goto(warmupUrl, { timeout: 15e3 });
338
- await page.waitForLoadState("networkidle");
461
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 });
339
462
  console.log("\u2705 Storybook warmed up");
340
463
  } catch {
341
464
  console.log("\u26A0\uFE0F Warmup had issues, proceeding anyway");
@@ -358,6 +481,8 @@ async function generateScreenshots(options = {}) {
358
481
  const { command: command2, args } = getStorybookCommand(storybookDir);
359
482
  storybookProcess = await startStorybook(command2, args, storybookDir);
360
483
  weStartedStorybook = true;
484
+ console.log("\u23F3 Waiting for Storybook to finish compiling...");
485
+ await waitForStorybookReady(url);
361
486
  }
362
487
  const allStories = await fetchStoryIndex(url);
363
488
  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 };