@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 +1 -1
- package/dist/cli/index.js +160 -35
- package/dist/index.d.ts +11 -1
- package/dist/index.js +97310 -464
- package/dist/screenshot-service/index.js +104 -23
- package/dist/screenshot-service/utils/browser/index.js +2 -1
- package/package.json +4 -3
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.
|
|
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
|
-
|
|
100
|
+
try {
|
|
101
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
99
104
|
await page.evaluate(() => document.fonts.ready);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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", ...
|
|
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
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
|
427
|
+
reject(new Error("Storybook failed to start within 180 seconds"));
|
|
305
428
|
}
|
|
306
|
-
},
|
|
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("
|
|
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
|
-
|
|
19
|
+
declare const tolerantCsfIndexer: Indexer;
|
|
20
|
+
|
|
21
|
+
export { type OnlookPluginOptions, storybookOnlookPlugin, tolerantCsfIndexer };
|