@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.
@@ -1,9 +1,13 @@
1
+ import { createRequire } from 'node:module';
1
2
  import crypto from 'crypto';
2
3
  import fs from 'fs';
3
- import path from 'path';
4
+ import path, { join, dirname } from 'path';
5
+ import { execFile } from 'child_process';
6
+ import { createRequire as createRequire$1 } from 'module';
7
+ import { promisify } from 'util';
4
8
  import { chromium } from 'playwright';
5
9
 
6
- // src/utils/fileSystem/fileSystem.ts
10
+ globalThis.require = createRequire(import.meta.url);
7
11
  var CACHE_DIR = path.join(process.cwd(), ".storybook-cache");
8
12
  var SCREENSHOTS_DIR = path.join(CACHE_DIR, "screenshots");
9
13
  var MANIFEST_PATH = path.join(CACHE_DIR, "manifest.json");
@@ -53,24 +57,88 @@ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
53
57
  };
54
58
  saveManifest(manifest);
55
59
  }
56
- var browser = null;
60
+ var execFileAsync = promisify(execFile);
61
+ var require2 = createRequire$1(import.meta.url);
62
+ var browserPromise = null;
63
+ var installPromise = null;
64
+ var isMissingExecutableError = (err) => {
65
+ const msg = err instanceof Error ? err.message : String(err);
66
+ 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.
67
+ /chromium-\d+\/chrome-linux\/chrome/i.test(msg);
68
+ };
69
+ function resolvePlaywrightCli() {
70
+ const pkgJson = require2.resolve("playwright-core/package.json");
71
+ return join(dirname(pkgJson), "cli.js");
72
+ }
73
+ async function installChromium() {
74
+ if (!installPromise) {
75
+ installPromise = (async () => {
76
+ const cliPath = resolvePlaywrightCli();
77
+ console.log(
78
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
79
+ );
80
+ try {
81
+ const { stdout, stderr } = await execFileAsync(
82
+ process.execPath,
83
+ [cliPath, "install", "--force", "chromium"],
84
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
85
+ );
86
+ if (stdout)
87
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
88
+ if (stderr)
89
+ console.log(
90
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
91
+ );
92
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
93
+ } catch (err) {
94
+ const detail = err instanceof Error ? err.message : String(err);
95
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
96
+ throw new Error(`Playwright chromium install failed: ${detail}`);
97
+ } finally {
98
+ installPromise = null;
99
+ }
100
+ })();
101
+ }
102
+ return installPromise;
103
+ }
104
+ async function launchWithSelfHeal() {
105
+ try {
106
+ return await chromium.launch({ headless: true });
107
+ } catch (err) {
108
+ if (!isMissingExecutableError(err)) throw err;
109
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
110
+ await installChromium();
111
+ try {
112
+ return await chromium.launch({ headless: true });
113
+ } catch (postInstallErr) {
114
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
115
+ console.error(
116
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
117
+ );
118
+ throw new Error(
119
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
120
+ );
121
+ }
122
+ }
123
+ }
57
124
  async function getBrowser() {
58
- if (!browser) {
59
- browser = await chromium.launch({
60
- headless: true
125
+ if (!browserPromise) {
126
+ browserPromise = launchWithSelfHeal().catch((err) => {
127
+ browserPromise = null;
128
+ throw err;
61
129
  });
62
130
  }
63
- return browser;
131
+ return browserPromise;
64
132
  }
65
133
  async function closeBrowser() {
66
- if (browser) {
67
- try {
68
- await browser.close();
69
- } catch (error) {
70
- console.error("Error closing browser:", error);
71
- } finally {
72
- browser = null;
73
- }
134
+ const pending = browserPromise;
135
+ if (!pending) return;
136
+ browserPromise = null;
137
+ try {
138
+ const b = await pending;
139
+ await b.close();
140
+ } catch (error) {
141
+ console.error("Error closing browser:", error);
74
142
  }
75
143
  }
76
144
  function getScreenshotPath(storyId, theme) {
@@ -82,8 +150,8 @@ function screenshotExists(storyId, theme) {
82
150
  return fs.existsSync(screenshotPath);
83
151
  }
84
152
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
85
- const browser2 = await getBrowser();
86
- const context = await browser2.newContext({
153
+ const browser = await getBrowser();
154
+ const context = await browser.newContext({
87
155
  viewport: { width, height },
88
156
  deviceScaleFactor: 2
89
157
  });
@@ -188,6 +256,61 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
188
256
  }
189
257
 
190
258
  // src/screenshot-service/screenshot-service.ts
259
+ var ONBOOK_PROXY_TOKEN = process.env.ONBOOK_PROXY_TOKEN ?? null;
260
+ var ONBOOK_BROADCAST_URL = process.env.ONBOOK_BROADCAST_URL ?? null;
261
+ var PROGRESS_TIMEOUT_MS = 3e3;
262
+ async function broadcastScreenshotProgress(completed, total) {
263
+ if (!ONBOOK_PROXY_TOKEN || !ONBOOK_BROADCAST_URL) return;
264
+ const percent = total > 0 ? Math.min(100, Math.floor(completed / total * 100)) : null;
265
+ try {
266
+ await fetch(ONBOOK_BROADCAST_URL, {
267
+ method: "POST",
268
+ headers: {
269
+ Authorization: `Bearer ${ONBOOK_PROXY_TOKEN}`,
270
+ "Content-Type": "application/json"
271
+ },
272
+ body: JSON.stringify({
273
+ event: {
274
+ type: "SANDBOX_PHASE_PROGRESS",
275
+ phase: "generating_screenshots",
276
+ completed,
277
+ total,
278
+ percent,
279
+ label: "Capturing stories"
280
+ }
281
+ }),
282
+ signal: AbortSignal.timeout(PROGRESS_TIMEOUT_MS)
283
+ });
284
+ } catch {
285
+ }
286
+ }
287
+ var StoryTimeoutError = class extends Error {
288
+ constructor(storyId, timeoutMs) {
289
+ super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
290
+ this.storyId = storyId;
291
+ this.timeoutMs = timeoutMs;
292
+ this.name = "StoryTimeoutError";
293
+ }
294
+ };
295
+ async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
296
+ let timer;
297
+ try {
298
+ return await Promise.race([
299
+ Promise.all([
300
+ generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
301
+ generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
302
+ ]),
303
+ new Promise((_, reject) => {
304
+ timer = setTimeout(
305
+ () => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
306
+ storyTimeoutMs
307
+ );
308
+ })
309
+ ]);
310
+ } finally {
311
+ if (timer) clearTimeout(timer);
312
+ }
313
+ }
191
314
  async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
192
315
  const displayTotal = total ?? offset + stories.length;
193
316
  console.log(
@@ -198,58 +321,82 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
198
321
  for (let i = 0; i < stories.length; i += BATCH_SIZE) {
199
322
  batches.push(stories.slice(i, i + BATCH_SIZE));
200
323
  }
324
+ const storyTimeout = timeoutMs * 2 + 1e4;
201
325
  let completed = 0;
326
+ void broadcastScreenshotProgress(0, displayTotal);
202
327
  for (const batch of batches) {
203
328
  await Promise.all(
204
329
  batch.map(async (story) => {
205
330
  if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
206
331
  completed++;
207
- const absoluteIndex = offset + completed;
208
- console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
332
+ const absoluteIndex2 = offset + completed;
333
+ console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
209
334
  return;
210
335
  }
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);
336
+ let lightResult = null;
337
+ let darkResult = null;
338
+ let lastError;
339
+ const startedAt = Date.now();
340
+ let attempts = 0;
341
+ const maxAttempts = 2;
342
+ while (attempts < maxAttempts) {
343
+ attempts++;
344
+ try {
345
+ [lightResult, darkResult] = await captureBothThemes(
346
+ story.id,
347
+ storybookUrl,
348
+ timeoutMs,
349
+ storyTimeout
350
+ );
351
+ lastError = void 0;
352
+ break;
353
+ } catch (error) {
354
+ lastError = error;
355
+ if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
356
+ break;
357
+ }
358
+ console.warn(
359
+ `[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
360
+ );
235
361
  }
236
- completed++;
237
- const absoluteIndex = offset + completed;
362
+ }
363
+ completed++;
364
+ const absoluteIndex = offset + completed;
365
+ const durationMs = Date.now() - startedAt;
366
+ if (lightResult && darkResult) {
367
+ const fileHash = computeFileHash(story.importPath);
368
+ updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
238
369
  console.log(
239
- `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
370
+ `[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
371
+ { storyId: story.id, attempts, durationMs, outcome: "ok" }
240
372
  );
241
- } catch (error) {
242
- completed++;
243
- const absoluteIndex = offset + completed;
373
+ } else if (lastError) {
244
374
  console.error(
245
375
  `[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
246
- error instanceof Error ? error.message : error
376
+ lastError instanceof Error ? lastError.message : lastError,
377
+ {
378
+ storyId: story.id,
379
+ attempts,
380
+ durationMs,
381
+ outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
382
+ }
247
383
  );
384
+ } else {
385
+ console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
386
+ storyId: story.id,
387
+ attempts,
388
+ durationMs,
389
+ outcome: "partial",
390
+ hasLight: !!lightResult,
391
+ hasDark: !!darkResult
392
+ });
248
393
  }
249
394
  })
250
395
  );
396
+ void broadcastScreenshotProgress(offset + completed, displayTotal);
251
397
  }
252
398
  await closeBrowser();
399
+ void broadcastScreenshotProgress(displayTotal, displayTotal);
253
400
  console.log("Screenshot generation complete!");
254
401
  }
255
402
 
@@ -1,12 +1,6 @@
1
1
  import { Browser } from 'playwright';
2
2
 
3
- /**
4
- * Initialize browser instance
5
- */
6
3
  declare function getBrowser(): Promise<Browser>;
7
- /**
8
- * Close browser instance
9
- */
10
4
  declare function closeBrowser(): Promise<void>;
11
5
 
12
6
  export { closeBrowser, getBrowser };
@@ -1,24 +1,93 @@
1
+ import { createRequire } from 'node:module';
2
+ import { execFile } from 'child_process';
3
+ import { createRequire as createRequire$1 } from 'module';
4
+ import { join, dirname } from 'path';
5
+ import { promisify } from 'util';
1
6
  import { chromium } from 'playwright';
2
7
 
3
- // src/screenshot-service/utils/browser/browser.ts
4
- var browser = null;
8
+ globalThis.require = createRequire(import.meta.url);
9
+ var execFileAsync = promisify(execFile);
10
+ var require2 = createRequire$1(import.meta.url);
11
+ var browserPromise = null;
12
+ var installPromise = null;
13
+ var isMissingExecutableError = (err) => {
14
+ const msg = err instanceof Error ? err.message : String(err);
15
+ 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.
16
+ /chromium-\d+\/chrome-linux\/chrome/i.test(msg);
17
+ };
18
+ function resolvePlaywrightCli() {
19
+ const pkgJson = require2.resolve("playwright-core/package.json");
20
+ return join(dirname(pkgJson), "cli.js");
21
+ }
22
+ async function installChromium() {
23
+ if (!installPromise) {
24
+ installPromise = (async () => {
25
+ const cliPath = resolvePlaywrightCli();
26
+ console.log(
27
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
28
+ );
29
+ try {
30
+ const { stdout, stderr } = await execFileAsync(
31
+ process.execPath,
32
+ [cliPath, "install", "--force", "chromium"],
33
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
34
+ );
35
+ if (stdout)
36
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
37
+ if (stderr)
38
+ console.log(
39
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
40
+ );
41
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
42
+ } catch (err) {
43
+ const detail = err instanceof Error ? err.message : String(err);
44
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
45
+ throw new Error(`Playwright chromium install failed: ${detail}`);
46
+ } finally {
47
+ installPromise = null;
48
+ }
49
+ })();
50
+ }
51
+ return installPromise;
52
+ }
53
+ async function launchWithSelfHeal() {
54
+ try {
55
+ return await chromium.launch({ headless: true });
56
+ } catch (err) {
57
+ if (!isMissingExecutableError(err)) throw err;
58
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
59
+ await installChromium();
60
+ try {
61
+ return await chromium.launch({ headless: true });
62
+ } catch (postInstallErr) {
63
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
64
+ console.error(
65
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
66
+ );
67
+ throw new Error(
68
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
69
+ );
70
+ }
71
+ }
72
+ }
5
73
  async function getBrowser() {
6
- if (!browser) {
7
- browser = await chromium.launch({
8
- headless: true
74
+ if (!browserPromise) {
75
+ browserPromise = launchWithSelfHeal().catch((err) => {
76
+ browserPromise = null;
77
+ throw err;
9
78
  });
10
79
  }
11
- return browser;
80
+ return browserPromise;
12
81
  }
13
82
  async function closeBrowser() {
14
- if (browser) {
15
- try {
16
- await browser.close();
17
- } catch (error) {
18
- console.error("Error closing browser:", error);
19
- } finally {
20
- browser = null;
21
- }
83
+ const pending = browserPromise;
84
+ if (!pending) return;
85
+ browserPromise = null;
86
+ try {
87
+ const b = await pending;
88
+ await b.close();
89
+ } catch (error) {
90
+ console.error("Error closing browser:", error);
22
91
  }
23
92
  }
24
93
 
@@ -0,0 +1,61 @@
1
+ import { Plugin, PluginOption } from 'vite';
2
+
3
+ /** One module the engine asked us to substitute with a deferred-throw stand-in. */
4
+ type EnvContainmentSubstitution = {
5
+ /**
6
+ * Path of the throwing module relative to the project root (the directory
7
+ * `storybook dev` runs from), posix separators, extension included —
8
+ * e.g. `src/env.ts`. Alias (`@/env`), relative (`../env`), and absolute
9
+ * imports that resolve to this file are all intercepted.
10
+ */
11
+ module: string;
12
+ /**
13
+ * Best-effort named exports of the real module. The stand-in re-exports
14
+ * each (plus a default export, always) so static ESM bindings keep
15
+ * resolving. Names that aren't valid identifiers are skipped.
16
+ */
17
+ exports?: string[];
18
+ /** Env keys the module validates — carried into the containment marker. */
19
+ envKeys?: string[];
20
+ /** Recognized schema library (`t3` | `zod` | `valibot`) — diagnostics only. */
21
+ schemaKind?: string;
22
+ };
23
+ /**
24
+ * The `envContainment` plugin-options surface. Written by the preflight
25
+ * engine into `.storybook/main.*` (`storybookOnlookPlugin({ envContainment })`).
26
+ * Mirrored structurally as `EnvContainmentValue` in
27
+ * `packages/preflight/src/checks/storybook/patch.ts` — keep the shapes in sync.
28
+ */
29
+ type EnvContainmentOptions = {
30
+ /** Modules to replace with deferred-throw stand-ins. */
31
+ substitute?: EnvContainmentSubstitution[];
32
+ /**
33
+ * Env key → placeholder value. Each key is injected via Vite `define` as
34
+ * both `process.env.<KEY>` and `import.meta.env.<KEY>`.
35
+ */
36
+ define?: Record<string, string>;
37
+ };
38
+ /**
39
+ * Build the env-containment Vite plugin. Returns `null` when the options
40
+ * carry no work — callers spread it conditionally so a config without
41
+ * `envContainment` behaves exactly as before this plugin existed.
42
+ */
43
+ declare function envContainmentPlugin(options: EnvContainmentOptions | undefined): Plugin | null;
44
+
45
+ type OnlookPluginOptions = {
46
+ /** Storybook port (default: 6006) */
47
+ port?: number;
48
+ /** Additional allowed origins for CORS (merged with defaults) */
49
+ allowedOrigins?: string[];
50
+ hmr?: {
51
+ protocol?: 'ws' | 'wss';
52
+ clientPort?: number;
53
+ };
54
+ storyGlobs?: string[];
55
+ tailwindEntryCss?: string | false;
56
+ envContainment?: EnvContainmentOptions;
57
+ moduleLoadCatchAll?: boolean;
58
+ };
59
+ declare function storybookOnlookPlugin(options?: OnlookPluginOptions): PluginOption[];
60
+
61
+ export { type EnvContainmentOptions as E, type OnlookPluginOptions as O, type EnvContainmentSubstitution as a, envContainmentPlugin as e, storybookOnlookPlugin as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.4.0-beta.1",
3
+ "version": "0.4.0-beta.10",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "onlook-storybook": "./dist/cli/index.js"
@@ -10,6 +10,14 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
+ "./preset": {
14
+ "types": "./dist/preset/index.d.ts",
15
+ "default": "./dist/preset/index.js"
16
+ },
17
+ "./preview": {
18
+ "types": "./dist/preview/index.d.ts",
19
+ "default": "./dist/preview/index.js"
20
+ },
13
21
  "./screenshot-service": {
14
22
  "types": "./dist/screenshot-service/index.d.ts",
15
23
  "default": "./dist/screenshot-service/index.js"
@@ -24,25 +32,26 @@
24
32
  "README.md"
25
33
  ],
26
34
  "scripts": {
27
- "build": "tsup",
35
+ "build": "rm -rf dist && tsup",
28
36
  "clean": "git clean -xdf .cache .turbo dist node_modules",
29
37
  "typecheck": "tsc --noEmit",
30
38
  "prepublishOnly": "bun run build && bun scripts/prepublish.ts",
31
39
  "postpublish": "bun scripts/postpublish.ts",
32
- "publish-pkg": "npm publish"
40
+ "publish-pkg": "bun publish",
41
+ "publish-beta": "bun publish --tag beta"
33
42
  },
34
43
  "dependencies": {
35
44
  "@babel/generator": "^7.26.9",
36
- "@babel/parser": "^7.26.9",
45
+ "@babel/parser": "^7.29.3",
37
46
  "@babel/traverse": "^7.26.9",
38
- "@babel/types": "^7.26.9",
47
+ "@babel/types": "^7.29.0",
39
48
  "@drizzle-team/brocli": "^0.11.0",
40
- "@takuma-ru/auto-story-generator": "^0.4.0",
41
49
  "playwright": "^1.52.0",
42
- "react-docgen-typescript": "^2.4.0"
50
+ "playwright-core": "^1.52.0",
51
+ "storybook": "^10.1.11"
43
52
  },
44
53
  "devDependencies": {
45
- "@onbook/tsconfig": "workspace:*",
54
+ "@onbook/tsconfig": "0.1.0",
46
55
  "@types/babel__generator": "^7.6.8",
47
56
  "@types/babel__traverse": "^7.20.6",
48
57
  "@types/node": "^22.15.32",
@@ -55,6 +64,7 @@
55
64
  "access": "public"
56
65
  },
57
66
  "peerDependencies": {
58
- "vite": "^5.0.0 || ^6.0.0"
67
+ "react": "^18.0.0 || ^19.0.0",
68
+ "vite": ">=5.0.0"
59
69
  }
60
70
  }