@onlook/storybook-plugin 0.3.2 → 0.3.4

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
 
@@ -76,6 +76,27 @@ Screenshots are saved to `.storybook-cache/screenshots/`.
76
76
 
77
77
  The plugin auto-disables when `CHROMATIC` or `CI` environment variables are set.
78
78
 
79
+ ## Publishing
80
+
81
+ Three release paths:
82
+
83
+ ```bash
84
+ # 1. Ship the current version to npm as `latest` (default dist-tag).
85
+ bun run publish-pkg
86
+
87
+ # 2. Ship under the `next` dist-tag — for testing against preview deploys
88
+ # without affecting consumers pinned to ^x.y.z ranges. Consumers opt in
89
+ # via `@onlook/storybook-plugin@next`.
90
+ bun run publish-pkg:next
91
+
92
+ # 3. Promote a previously-published version to `latest`. Reads the version
93
+ # from package.json, so bump first if you want to promote something else.
94
+ bun run promote-latest
95
+ ```
96
+
97
+ The typical flow for risky changes: `publish-pkg:next` → test against a
98
+ Vercel preview deploy → `promote-latest` once confident.
99
+
79
100
  ## License
80
101
 
81
102
  MIT
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.d.ts CHANGED
@@ -1,4 +1,5 @@
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) */
@@ -8,4 +9,6 @@ type OnlookPluginOptions = {
8
9
  };
9
10
  declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
10
11
 
11
- export { type OnlookPluginOptions, storybookOnlookPlugin };
12
+ declare const tolerantCsfIndexer: Indexer;
13
+
14
+ export { type OnlookPluginOptions, storybookOnlookPlugin, tolerantCsfIndexer };
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import traverseModule from '@babel/traverse';
7
7
  import * as t from '@babel/types';
8
8
  import crypto from 'crypto';
9
9
  import { chromium } from 'playwright';
10
+ import { readCsf } from 'storybook/internal/csf-tools';
10
11
 
11
12
  // src/storybook-onlook-plugin.ts
12
13
  function componentLocPlugin(options = {}) {
@@ -142,20 +143,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
142
143
  await page.goto(url, { timeout: timeoutMs });
143
144
  await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
144
145
  await page.waitForLoadState("load", { timeout: timeoutMs });
145
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
146
+ try {
147
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
148
+ } catch {
149
+ }
146
150
  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
- });
151
+ try {
152
+ await page.evaluate(async () => {
153
+ const images = document.querySelectorAll("img");
154
+ await Promise.all(
155
+ Array.from(images).map((img) => {
156
+ if (img.complete) return Promise.resolve();
157
+ return new Promise((resolve) => {
158
+ img.addEventListener("load", resolve);
159
+ img.addEventListener("error", resolve);
160
+ setTimeout(resolve, 3e3);
161
+ });
162
+ })
163
+ );
164
+ });
165
+ } catch {
166
+ }
159
167
  const contentBounds = await page.evaluate(() => {
160
168
  const root = document.querySelector("#storybook-root");
161
169
  if (!root) return null;
@@ -331,7 +339,7 @@ var gitRoot = findGitRoot(storybookDir);
331
339
  var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
332
340
  var repoRoot = gitRoot || process.cwd();
333
341
  var DEFAULT_ALLOWED_ORIGINS = [
334
- "https://app.onlook.ai",
342
+ "https://app.onlook.com",
335
343
  "http://localhost:3000",
336
344
  "http://localhost:6006"
337
345
  ];
@@ -496,17 +504,16 @@ function storybookOnlookPlugin(options = {}) {
496
504
  const mainPlugin = {
497
505
  name: "storybook-onlook-plugin",
498
506
  config() {
499
- const isE2B = !!process.env.E2B_SANDBOX;
500
507
  return {
501
508
  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
- }
509
+ // E2B sandbox HMR configuration
510
+ hmr: {
511
+ // E2B sandboxes use HTTPS, so we need secure WebSocket
512
+ protocol: "wss",
513
+ // E2B routes through standard HTTPS port 443
514
+ clientPort: 443,
515
+ // The actual Storybook server port inside the sandbox
516
+ port
510
517
  },
511
518
  cors: {
512
519
  origin: allowedOrigins
@@ -528,15 +535,21 @@ function storybookOnlookPlugin(options = {}) {
528
535
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
529
536
  server.middlewares.use(serveMetadataAndScreenshots);
530
537
  },
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
- }
538
+ handleHotUpdate: handleStoryFileChange
538
539
  };
539
540
  return [componentLocPlugin(), mainPlugin];
540
541
  }
542
+ var tolerantCsfIndexer = {
543
+ test: /(stories|story)\.(m?js|ts)x?$/,
544
+ createIndex: async (fileName, options) => {
545
+ try {
546
+ return (await readCsf(fileName, options)).parse().indexInputs;
547
+ } catch (err) {
548
+ const msg = err instanceof Error ? err.message : String(err);
549
+ console.warn("[onbook] Skipping broken story file:", fileName, msg);
550
+ return [];
551
+ }
552
+ }
553
+ };
541
554
 
542
- export { storybookOnlookPlugin };
555
+ export { storybookOnlookPlugin, tolerantCsfIndexer };
@@ -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,9 +1,9 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "bin": {
6
- "onlook-storybook": "./dist/cli/index.js"
6
+ "onlook-storybook": "dist/cli/index.js"
7
7
  },
8
8
  "exports": {
9
9
  ".": {
@@ -29,7 +29,9 @@
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"
32
+ "publish-pkg": "npm publish",
33
+ "publish-pkg:next": "npm publish --tag next",
34
+ "promote-latest": "npm dist-tag add @onlook/storybook-plugin@$(node -p \"require('./package.json').version\") latest"
33
35
  },
34
36
  "dependencies": {
35
37
  "@babel/generator": "^7.26.9",
@@ -53,6 +55,7 @@
53
55
  "access": "public"
54
56
  },
55
57
  "peerDependencies": {
58
+ "storybook": "^10.0.0",
56
59
  "vite": "^5.0.0 || ^6.0.0"
57
60
  }
58
61
  }