@onlook/storybook-plugin 0.4.0-beta.1 → 0.4.0-beta.10

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,88 @@ 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
+ function resolvePlaywrightCli() {
72
+ const pkgJson = require2.resolve("playwright-core/package.json");
73
+ return join(dirname(pkgJson), "cli.js");
74
+ }
75
+ async function installChromium() {
76
+ if (!installPromise) {
77
+ installPromise = (async () => {
78
+ const cliPath = resolvePlaywrightCli();
79
+ console.log(
80
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
81
+ );
82
+ try {
83
+ const { stdout, stderr } = await execFileAsync(
84
+ process.execPath,
85
+ [cliPath, "install", "--force", "chromium"],
86
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
87
+ );
88
+ if (stdout)
89
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
90
+ if (stderr)
91
+ console.log(
92
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
93
+ );
94
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
95
+ } catch (err) {
96
+ const detail = err instanceof Error ? err.message : String(err);
97
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
98
+ throw new Error(`Playwright chromium install failed: ${detail}`);
99
+ } finally {
100
+ installPromise = null;
101
+ }
102
+ })();
103
+ }
104
+ return installPromise;
105
+ }
106
+ async function launchWithSelfHeal() {
107
+ try {
108
+ return await chromium.launch({ headless: true });
109
+ } catch (err) {
110
+ if (!isMissingExecutableError(err)) throw err;
111
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
112
+ await installChromium();
113
+ try {
114
+ return await chromium.launch({ headless: true });
115
+ } catch (postInstallErr) {
116
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
117
+ console.error(
118
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
119
+ );
120
+ throw new Error(
121
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
122
+ );
123
+ }
124
+ }
125
+ }
59
126
  async function getBrowser() {
60
- if (!browser) {
61
- browser = await chromium.launch({
62
- headless: true
127
+ if (!browserPromise) {
128
+ browserPromise = launchWithSelfHeal().catch((err) => {
129
+ browserPromise = null;
130
+ throw err;
63
131
  });
64
132
  }
65
- return browser;
133
+ return browserPromise;
66
134
  }
67
135
  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
- }
136
+ const pending = browserPromise;
137
+ if (!pending) return;
138
+ browserPromise = null;
139
+ try {
140
+ const b = await pending;
141
+ await b.close();
142
+ } catch (error) {
143
+ console.error("Error closing browser:", error);
76
144
  }
77
145
  }
78
146
  function getScreenshotPath(storyId, theme) {
@@ -84,8 +152,8 @@ function screenshotExists(storyId, theme) {
84
152
  return fs.existsSync(screenshotPath);
85
153
  }
86
154
  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({
155
+ const browser = await getBrowser();
156
+ const context = await browser.newContext({
89
157
  viewport: { width, height },
90
158
  deviceScaleFactor: 2
91
159
  });
@@ -190,6 +258,61 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
190
258
  }
191
259
 
192
260
  // src/screenshot-service/screenshot-service.ts
261
+ var ONBOOK_PROXY_TOKEN = process.env.ONBOOK_PROXY_TOKEN ?? null;
262
+ var ONBOOK_BROADCAST_URL = process.env.ONBOOK_BROADCAST_URL ?? null;
263
+ var PROGRESS_TIMEOUT_MS = 3e3;
264
+ async function broadcastScreenshotProgress(completed, total) {
265
+ if (!ONBOOK_PROXY_TOKEN || !ONBOOK_BROADCAST_URL) return;
266
+ const percent = total > 0 ? Math.min(100, Math.floor(completed / total * 100)) : null;
267
+ try {
268
+ await fetch(ONBOOK_BROADCAST_URL, {
269
+ method: "POST",
270
+ headers: {
271
+ Authorization: `Bearer ${ONBOOK_PROXY_TOKEN}`,
272
+ "Content-Type": "application/json"
273
+ },
274
+ body: JSON.stringify({
275
+ event: {
276
+ type: "SANDBOX_PHASE_PROGRESS",
277
+ phase: "generating_screenshots",
278
+ completed,
279
+ total,
280
+ percent,
281
+ label: "Capturing stories"
282
+ }
283
+ }),
284
+ signal: AbortSignal.timeout(PROGRESS_TIMEOUT_MS)
285
+ });
286
+ } catch {
287
+ }
288
+ }
289
+ var StoryTimeoutError = class extends Error {
290
+ constructor(storyId, timeoutMs) {
291
+ super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
292
+ this.storyId = storyId;
293
+ this.timeoutMs = timeoutMs;
294
+ this.name = "StoryTimeoutError";
295
+ }
296
+ };
297
+ async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
298
+ let timer;
299
+ try {
300
+ return await Promise.race([
301
+ Promise.all([
302
+ generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
303
+ generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
304
+ ]),
305
+ new Promise((_, reject) => {
306
+ timer = setTimeout(
307
+ () => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
308
+ storyTimeoutMs
309
+ );
310
+ })
311
+ ]);
312
+ } finally {
313
+ if (timer) clearTimeout(timer);
314
+ }
315
+ }
193
316
  async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
194
317
  const displayTotal = total ?? offset + stories.length;
195
318
  console.log(
@@ -200,58 +323,82 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
200
323
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
201
324
  batches.push(stories.slice(i, i + BATCH_SIZE));
202
325
  }
326
+ const storyTimeout = timeoutMs * 2 + 1e4;
203
327
  let completed = 0;
328
+ void broadcastScreenshotProgress(0, displayTotal);
204
329
  for (const batch of batches) {
205
330
  await Promise.all(
206
331
  batch.map(async (story) => {
207
332
  if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
208
333
  completed++;
209
- const absoluteIndex = offset + completed;
210
- console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
334
+ const absoluteIndex2 = offset + completed;
335
+ console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
211
336
  return;
212
337
  }
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);
338
+ let lightResult = null;
339
+ let darkResult = null;
340
+ let lastError;
341
+ const startedAt = Date.now();
342
+ let attempts = 0;
343
+ const maxAttempts = 2;
344
+ while (attempts < maxAttempts) {
345
+ attempts++;
346
+ try {
347
+ [lightResult, darkResult] = await captureBothThemes(
348
+ story.id,
349
+ storybookUrl,
350
+ timeoutMs,
351
+ storyTimeout
352
+ );
353
+ lastError = void 0;
354
+ break;
355
+ } catch (error) {
356
+ lastError = error;
357
+ if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
358
+ break;
359
+ }
360
+ console.warn(
361
+ `[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
362
+ );
237
363
  }
238
- completed++;
239
- const absoluteIndex = offset + completed;
364
+ }
365
+ completed++;
366
+ const absoluteIndex = offset + completed;
367
+ const durationMs = Date.now() - startedAt;
368
+ if (lightResult && darkResult) {
369
+ const fileHash = computeFileHash(story.importPath);
370
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
240
371
  console.log(
241
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
372
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
373
+ { storyId: story.id, attempts, durationMs, outcome: "ok" }
242
374
  );
243
- } catch (error) {
244
- completed++;
245
- const absoluteIndex = offset + completed;
375
+ } else if (lastError) {
246
376
  console.error(
247
377
  `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
248
- error instanceof Error ? error.message : error
378
+ lastError instanceof Error ? lastError.message : lastError,
379
+ {
380
+ storyId: story.id,
381
+ attempts,
382
+ durationMs,
383
+ outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
384
+ }
249
385
  );
386
+ } else {
387
+ console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
388
+ storyId: story.id,
389
+ attempts,
390
+ durationMs,
391
+ outcome: "partial",
392
+ hasLight: !!lightResult,
393
+ hasDark: !!darkResult
394
+ });
250
395
  }
251
396
  })
252
397
  );
398
+ void broadcastScreenshotProgress(offset + completed, displayTotal);
253
399
  }
254
400
  await closeBrowser();
401
+ void broadcastScreenshotProgress(displayTotal, displayTotal);
255
402
  console.log("Screenshot generation complete!");
256
403
  }
257
404
  var LOCKFILES = {
@@ -402,8 +549,8 @@ async function startStorybook(command2, args, storybookDir) {
402
549
  }
403
550
  async function warmupStorybook(url, firstStoryId) {
404
551
  console.log("\u{1F525} Warming up Storybook...");
405
- const browser2 = await getBrowser();
406
- const context = await browser2.newContext();
552
+ const browser = await getBrowser();
553
+ const context = await browser.newContext();
407
554
  const page = await context.newPage();
408
555
  try {
409
556
  const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
@@ -509,5 +656,5 @@ var generateScreenshotsCommand = command({
509
656
  });
510
657
  run([generateScreenshotsCommand], {
511
658
  name: "onlook-storybook",
512
- description: "Storybook plugin for Onbook - generate screenshots and more"
659
+ description: "Storybook plugin for Onlook - generate screenshots and more"
513
660
  });
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 };