@onlook/storybook-plugin 0.4.0-beta.6 → 0.4.0-beta.9

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,15 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import { command, string, boolean, run } from '@drizzle-team/brocli';
3
- import { spawn } from 'child_process';
4
+ import { execFile, spawn } from 'child_process';
4
5
  import fs, { readFileSync, existsSync } from 'fs';
5
6
  import path, { resolve, join, dirname } from 'path';
6
7
  import crypto from 'crypto';
8
+ import { createRequire as createRequire$1 } from 'module';
9
+ import { promisify } from 'util';
7
10
  import { chromium } from 'playwright';
8
11
 
12
+ globalThis.require = createRequire(import.meta.url);
9
13
  var CACHE_DIR = path.join(process.cwd(), ".storybook-cache");
10
14
  var SCREENSHOTS_DIR = path.join(CACHE_DIR, "screenshots");
11
15
  var MANIFEST_PATH = path.join(CACHE_DIR, "manifest.json");
@@ -55,24 +59,84 @@ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
55
59
  };
56
60
  saveManifest(manifest);
57
61
  }
58
- var browser = null;
62
+ var execFileAsync = promisify(execFile);
63
+ var require2 = createRequire$1(import.meta.url);
64
+ var browserPromise = null;
65
+ var installPromise = null;
66
+ var isMissingExecutableError = (err) => {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ return msg.includes("Executable doesn't exist") || msg.includes("Please run the following command to download new browsers") || // Older playwright revisions phrase the version-mismatch case differently.
69
+ /chromium-\d+\/chrome-linux\/chrome/i.test(msg);
70
+ };
71
+ async function installChromium() {
72
+ if (!installPromise) {
73
+ installPromise = (async () => {
74
+ const cliPath = require2.resolve("playwright-core/cli.js");
75
+ console.log(
76
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
77
+ );
78
+ try {
79
+ const { stdout, stderr } = await execFileAsync(
80
+ process.execPath,
81
+ [cliPath, "install", "--force", "chromium"],
82
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
83
+ );
84
+ if (stdout)
85
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
86
+ if (stderr)
87
+ console.log(
88
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
89
+ );
90
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
91
+ } catch (err) {
92
+ const detail = err instanceof Error ? err.message : String(err);
93
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
94
+ throw new Error(`Playwright chromium install failed: ${detail}`);
95
+ } finally {
96
+ installPromise = null;
97
+ }
98
+ })();
99
+ }
100
+ return installPromise;
101
+ }
102
+ async function launchWithSelfHeal() {
103
+ try {
104
+ return await chromium.launch({ headless: true });
105
+ } catch (err) {
106
+ if (!isMissingExecutableError(err)) throw err;
107
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
108
+ await installChromium();
109
+ try {
110
+ return await chromium.launch({ headless: true });
111
+ } catch (postInstallErr) {
112
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
113
+ console.error(
114
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
115
+ );
116
+ throw new Error(
117
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
118
+ );
119
+ }
120
+ }
121
+ }
59
122
  async function getBrowser() {
60
- if (!browser) {
61
- browser = await chromium.launch({
62
- headless: true
123
+ if (!browserPromise) {
124
+ browserPromise = launchWithSelfHeal().catch((err) => {
125
+ browserPromise = null;
126
+ throw err;
63
127
  });
64
128
  }
65
- return browser;
129
+ return browserPromise;
66
130
  }
67
131
  async function closeBrowser() {
68
- if (browser) {
69
- try {
70
- await browser.close();
71
- } catch (error) {
72
- console.error("Error closing browser:", error);
73
- } finally {
74
- browser = null;
75
- }
132
+ const pending = browserPromise;
133
+ if (!pending) return;
134
+ browserPromise = null;
135
+ try {
136
+ const b = await pending;
137
+ await b.close();
138
+ } catch (error) {
139
+ console.error("Error closing browser:", error);
76
140
  }
77
141
  }
78
142
  function getScreenshotPath(storyId, theme) {
@@ -84,8 +148,8 @@ function screenshotExists(storyId, theme) {
84
148
  return fs.existsSync(screenshotPath);
85
149
  }
86
150
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
87
- const browser2 = await getBrowser();
88
- const context = await browser2.newContext({
151
+ const browser = await getBrowser();
152
+ const context = await browser.newContext({
89
153
  viewport: { width, height },
90
154
  deviceScaleFactor: 2
91
155
  });
@@ -190,6 +254,61 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
190
254
  }
191
255
 
192
256
  // src/screenshot-service/screenshot-service.ts
257
+ var ONBOOK_PROXY_TOKEN = process.env.ONBOOK_PROXY_TOKEN ?? null;
258
+ var ONBOOK_BROADCAST_URL = process.env.ONBOOK_BROADCAST_URL ?? null;
259
+ var PROGRESS_TIMEOUT_MS = 3e3;
260
+ async function broadcastScreenshotProgress(completed, total) {
261
+ if (!ONBOOK_PROXY_TOKEN || !ONBOOK_BROADCAST_URL) return;
262
+ const percent = total > 0 ? Math.min(100, Math.floor(completed / total * 100)) : null;
263
+ try {
264
+ await fetch(ONBOOK_BROADCAST_URL, {
265
+ method: "POST",
266
+ headers: {
267
+ Authorization: `Bearer ${ONBOOK_PROXY_TOKEN}`,
268
+ "Content-Type": "application/json"
269
+ },
270
+ body: JSON.stringify({
271
+ event: {
272
+ type: "SANDBOX_PHASE_PROGRESS",
273
+ phase: "generating_screenshots",
274
+ completed,
275
+ total,
276
+ percent,
277
+ label: "Capturing stories"
278
+ }
279
+ }),
280
+ signal: AbortSignal.timeout(PROGRESS_TIMEOUT_MS)
281
+ });
282
+ } catch {
283
+ }
284
+ }
285
+ var StoryTimeoutError = class extends Error {
286
+ constructor(storyId, timeoutMs) {
287
+ super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
288
+ this.storyId = storyId;
289
+ this.timeoutMs = timeoutMs;
290
+ this.name = "StoryTimeoutError";
291
+ }
292
+ };
293
+ async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
294
+ let timer;
295
+ try {
296
+ return await Promise.race([
297
+ Promise.all([
298
+ generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
299
+ generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
300
+ ]),
301
+ new Promise((_, reject) => {
302
+ timer = setTimeout(
303
+ () => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
304
+ storyTimeoutMs
305
+ );
306
+ })
307
+ ]);
308
+ } finally {
309
+ if (timer) clearTimeout(timer);
310
+ }
311
+ }
193
312
  async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
194
313
  const displayTotal = total ?? offset + stories.length;
195
314
  console.log(
@@ -200,58 +319,82 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
200
319
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
201
320
  batches.push(stories.slice(i, i + BATCH_SIZE));
202
321
  }
322
+ const storyTimeout = timeoutMs * 2 + 1e4;
203
323
  let completed = 0;
324
+ void broadcastScreenshotProgress(0, displayTotal);
204
325
  for (const batch of batches) {
205
326
  await Promise.all(
206
327
  batch.map(async (story) => {
207
328
  if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
208
329
  completed++;
209
- const absoluteIndex = offset + completed;
210
- console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
330
+ const absoluteIndex2 = offset + completed;
331
+ console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
211
332
  return;
212
333
  }
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);
334
+ let lightResult = null;
335
+ let darkResult = null;
336
+ let lastError;
337
+ const startedAt = Date.now();
338
+ let attempts = 0;
339
+ const maxAttempts = 2;
340
+ while (attempts < maxAttempts) {
341
+ attempts++;
342
+ try {
343
+ [lightResult, darkResult] = await captureBothThemes(
344
+ story.id,
345
+ storybookUrl,
346
+ timeoutMs,
347
+ storyTimeout
348
+ );
349
+ lastError = void 0;
350
+ break;
351
+ } catch (error) {
352
+ lastError = error;
353
+ if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
354
+ break;
355
+ }
356
+ console.warn(
357
+ `[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
358
+ );
237
359
  }
238
- completed++;
239
- const absoluteIndex = offset + completed;
360
+ }
361
+ completed++;
362
+ const absoluteIndex = offset + completed;
363
+ const durationMs = Date.now() - startedAt;
364
+ if (lightResult && darkResult) {
365
+ const fileHash = computeFileHash(story.importPath);
366
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
240
367
  console.log(
241
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
368
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
369
+ { storyId: story.id, attempts, durationMs, outcome: "ok" }
242
370
  );
243
- } catch (error) {
244
- completed++;
245
- const absoluteIndex = offset + completed;
371
+ } else if (lastError) {
246
372
  console.error(
247
373
  `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
248
- error instanceof Error ? error.message : error
374
+ lastError instanceof Error ? lastError.message : lastError,
375
+ {
376
+ storyId: story.id,
377
+ attempts,
378
+ durationMs,
379
+ outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
380
+ }
249
381
  );
382
+ } else {
383
+ console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
384
+ storyId: story.id,
385
+ attempts,
386
+ durationMs,
387
+ outcome: "partial",
388
+ hasLight: !!lightResult,
389
+ hasDark: !!darkResult
390
+ });
250
391
  }
251
392
  })
252
393
  );
394
+ void broadcastScreenshotProgress(offset + completed, displayTotal);
253
395
  }
254
396
  await closeBrowser();
397
+ void broadcastScreenshotProgress(displayTotal, displayTotal);
255
398
  console.log("Screenshot generation complete!");
256
399
  }
257
400
  var LOCKFILES = {
@@ -402,8 +545,8 @@ async function startStorybook(command2, args, storybookDir) {
402
545
  }
403
546
  async function warmupStorybook(url, firstStoryId) {
404
547
  console.log("\u{1F525} Warming up Storybook...");
405
- const browser2 = await getBrowser();
406
- const context = await browser2.newContext();
548
+ const browser = await getBrowser();
549
+ const context = await browser.newContext();
407
550
  const page = await context.newPage();
408
551
  try {
409
552
  const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
@@ -509,5 +652,5 @@ var generateScreenshotsCommand = command({
509
652
  });
510
653
  run([generateScreenshotsCommand], {
511
654
  name: "onlook-storybook",
512
- description: "Storybook plugin for Onbook - generate screenshots and more"
655
+ description: "Storybook plugin for Onlook - generate screenshots and more"
513
656
  });
package/dist/index.d.ts CHANGED
@@ -1,24 +1,97 @@
1
- import { PluginOption } from 'vite';
1
+ export { E as EnvContainmentOptions, a as EnvContainmentSubstitution, O as OnlookPluginOptions, e as envContainmentPlugin, s as storybookOnlookPlugin } from './storybook-onlook-plugin-CigILdDb.js';
2
+ import { Plugin } from 'vite';
3
+ import { Indexer } from 'storybook/internal/types';
2
4
 
3
- /** Folder name for auto-generated stories (created next to each component) */
4
- declare const AUTO_STORIES_FOLDER = ".onlook-stories";
5
- type OnlookPluginOptions = {
6
- /** Storybook port (default: 6006) */
7
- port?: number;
8
- /** Additional allowed origins for CORS (merged with defaults) */
9
- allowedOrigins?: string[];
10
- /**
11
- * Auto-generate stories for all components.
12
- * Glob patterns for component file discovery.
13
- * Set to false to disable. (default: ['src\/\*\*\/*.tsx'])
14
- */
15
- autoStories?: string[] | false;
16
- /**
17
- * Glob patterns to exclude from auto-story generation.
18
- * (default: stories, tests, and node_modules)
19
- */
20
- autoStoriesIgnore?: string[];
5
+ /** Advertised + embedded in every event payload. Mirrored CLI-side (U8). */
6
+ declare const ONLOOK_CONTAINMENT_CONTRACT_VERSION = 1;
7
+ /**
8
+ * Global registry the env-containment stand-ins populate at evaluation time:
9
+ * `Record<modulePath, ContainedModuleMarker>`.
10
+ */
11
+ declare const ONLOOK_CONTAINED_MODULES_GLOBAL = "__ONLOOK_CONTAINED_MODULES__";
12
+ /** Property carrying the marker on stand-in exports and thrown errors. */
13
+ declare const ONLOOK_CONTAINED_PROP = "__onlookContained";
14
+ /** Marker attached to every contained export, thrown error, and the global registry. */
15
+ type ContainedModuleMarker = {
16
+ modulePath: string;
17
+ reason: 'env-validation';
18
+ envKeys: string[];
19
+ schemaKind?: string;
21
20
  };
22
- declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
21
+ /**
22
+ * Handshake global, set by the preview annotations (in `beforeAll` and again
23
+ * defensively when the decorator first runs):
24
+ * `globalThis.__ONLOOK_CONTAINMENT_CONTRACT__ = { version: 1 }`.
25
+ */
26
+ declare const ONLOOK_CONTAINMENT_CONTRACT_GLOBAL = "__ONLOOK_CONTAINMENT_CONTRACT__";
27
+ type OnlookContainmentContract = {
28
+ version: number;
29
+ };
30
+ /**
31
+ * Emitted when the error-boundary decorator catches a render-time throw and
32
+ * renders the "needs setup" card in place of the story.
33
+ */
34
+ declare const ONLOOK_CONTAINED_EVENT = "onlook/contained";
35
+ /**
36
+ * Emitted once per story mount, after the boundary's children committed
37
+ * without throwing. The verifier must key `rendered` on THIS (plus absence of
38
+ * Storybook error events) — never on `STORY_RENDERED` alone, which Storybook
39
+ * fires even after its own boundary catches.
40
+ */
41
+ declare const ONLOOK_RENDERED_EVENT = "onlook/rendered";
42
+ /**
43
+ * Tag-for-verifier re-emit of errors the boundary cannot catch (loader /
44
+ * `beforeEach` / module-load throws surface as Storybook's
45
+ * `storyThrewException` / `storyErrored` channel events, outside the React
46
+ * render). These remain overlay-not-card; no DOM repaint is attempted.
47
+ */
48
+ declare const ONLOOK_UNCONTAINED_ERROR_EVENT = "onlook/uncontained-error";
49
+ type OnlookContainedPayload = {
50
+ storyId: string;
51
+ contractVersion: number;
52
+ /** Trimmed failure summary (error message). */
53
+ message: string;
54
+ /** Present when the failure is attributed to a contained module (U4). */
55
+ module?: ContainedModuleMarker;
56
+ };
57
+ type OnlookRenderedPayload = {
58
+ storyId: string;
59
+ contractVersion: number;
60
+ };
61
+ type OnlookUncontainedErrorPayload = {
62
+ /** Best-effort (read from the preview iframe URL); absent when unknown. */
63
+ storyId?: string;
64
+ contractVersion: number;
65
+ message: string;
66
+ /** Which Storybook channel event this re-emits. */
67
+ source: 'storyThrewException' | 'storyErrored';
68
+ };
69
+ /**
70
+ * Render-state attribute, value `'rendered' | 'contained'`. Set on
71
+ * `#storybook-root` (falling back to `document.body` when absent) after each
72
+ * boundary commit, and ALSO carried on the card's own wrapper element when
73
+ * contained. Cleared on boundary unmount (story switch), so a stale value
74
+ * never leaks into the next story's mount — verifiers should match it
75
+ * together with `data-onlook-story-id`.
76
+ */
77
+ declare const ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
78
+ /** Companion attribute carrying the story id the render state belongs to. */
79
+ declare const ONLOOK_RENDER_STORY_ATTR = "data-onlook-story-id";
80
+ /** Present (empty value) on the "needs setup" card's wrapper element. */
81
+ declare const ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
82
+ type OnlookRenderState = 'rendered' | 'contained';
83
+
84
+ /** Virtual stories module id as builder-vite declares it (SB_VIRTUAL_FILES). */
85
+ declare const STORYBOOK_VIRTUAL_STORIES_ID = "virtual:/@storybook/builder-vite/storybook-stories.js";
86
+ /** Rollup-resolved form (`getResolvedVirtualModuleId` prepends `\0`). */
87
+ declare const RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID = "\0virtual:/@storybook/builder-vite/storybook-stories.js";
88
+ declare const MODULE_LOAD_CATCH_ALL_PLUGIN_NAME = "onlook-module-load-catch-all";
89
+ /**
90
+ * The Vite plugin. Registered by `storybookOnlookPlugin()` unless the
91
+ * `moduleLoadCatchAll` option is `false`.
92
+ */
93
+ declare function moduleLoadCatchAllPlugin(): Plugin;
94
+
95
+ declare const tolerantCsfIndexer: Indexer;
23
96
 
24
- export { AUTO_STORIES_FOLDER, type OnlookPluginOptions, storybookOnlookPlugin };
97
+ export { type ContainedModuleMarker, MODULE_LOAD_CATCH_ALL_PLUGIN_NAME, ONLOOK_CONTAINED_CARD_ATTR, ONLOOK_CONTAINED_EVENT, ONLOOK_CONTAINED_MODULES_GLOBAL, ONLOOK_CONTAINED_PROP, ONLOOK_CONTAINMENT_CONTRACT_GLOBAL, ONLOOK_CONTAINMENT_CONTRACT_VERSION, ONLOOK_RENDERED_EVENT, ONLOOK_RENDER_STATE_ATTR, ONLOOK_RENDER_STORY_ATTR, ONLOOK_UNCONTAINED_ERROR_EVENT, type OnlookContainedPayload, type OnlookContainmentContract, type OnlookRenderState, type OnlookRenderedPayload, type OnlookUncontainedErrorPayload, RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID, STORYBOOK_VIRTUAL_STORIES_ID, moduleLoadCatchAllPlugin, tolerantCsfIndexer };