@onlook/storybook-plugin 0.3.1 → 0.3.2-beta.0
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 +158 -35
- package/dist/index.d.ts +11 -1
- package/dist/index.js +97308 -464
- package/dist/screenshot-service/index.js +102 -22
- 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,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
|
-
|
|
98
|
+
try {
|
|
99
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
99
102
|
await page.evaluate(() => document.fonts.ready);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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;
|
|
@@ -183,6 +190,33 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
|
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
// src/screenshot-service/screenshot-service.ts
|
|
193
|
+
var StoryTimeoutError = class extends Error {
|
|
194
|
+
constructor(storyId, timeoutMs) {
|
|
195
|
+
super(`Story ${storyId} timed out after ${timeoutMs / 1e3}s`);
|
|
196
|
+
this.storyId = storyId;
|
|
197
|
+
this.timeoutMs = timeoutMs;
|
|
198
|
+
this.name = "StoryTimeoutError";
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
async function captureBothThemes(storyId, storybookUrl, timeoutMs, storyTimeoutMs) {
|
|
202
|
+
let timer;
|
|
203
|
+
try {
|
|
204
|
+
return await Promise.race([
|
|
205
|
+
Promise.all([
|
|
206
|
+
generateScreenshot(storyId, "light", storybookUrl, timeoutMs),
|
|
207
|
+
generateScreenshot(storyId, "dark", storybookUrl, timeoutMs)
|
|
208
|
+
]),
|
|
209
|
+
new Promise((_, reject) => {
|
|
210
|
+
timer = setTimeout(
|
|
211
|
+
() => reject(new StoryTimeoutError(storyId, storyTimeoutMs)),
|
|
212
|
+
storyTimeoutMs
|
|
213
|
+
);
|
|
214
|
+
})
|
|
215
|
+
]);
|
|
216
|
+
} finally {
|
|
217
|
+
if (timer) clearTimeout(timer);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
186
220
|
async function generateAllScreenshots(stories, storybookUrl = "http://localhost:6006", concurrency = 10, offset = 0, total, skipExisting = false, timeoutMs = 3e4) {
|
|
187
221
|
const displayTotal = total ?? offset + stories.length;
|
|
188
222
|
console.log(
|
|
@@ -193,6 +227,7 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
193
227
|
for (let i = 0; i < stories.length; i += BATCH_SIZE) {
|
|
194
228
|
batches.push(stories.slice(i, i + BATCH_SIZE));
|
|
195
229
|
}
|
|
230
|
+
const storyTimeout = timeoutMs * 2 + 1e4;
|
|
196
231
|
let completed = 0;
|
|
197
232
|
for (const batch of batches) {
|
|
198
233
|
await Promise.all(
|
|
@@ -203,19 +238,64 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
203
238
|
console.log(`[${absoluteIndex2}/${displayTotal}] Skipped (exists) ${story.id}`);
|
|
204
239
|
return;
|
|
205
240
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
241
|
+
let lightResult = null;
|
|
242
|
+
let darkResult = null;
|
|
243
|
+
let lastError;
|
|
244
|
+
const startedAt = Date.now();
|
|
245
|
+
let attempts = 0;
|
|
246
|
+
const maxAttempts = 2;
|
|
247
|
+
while (attempts < maxAttempts) {
|
|
248
|
+
attempts++;
|
|
249
|
+
try {
|
|
250
|
+
[lightResult, darkResult] = await captureBothThemes(
|
|
251
|
+
story.id,
|
|
252
|
+
storybookUrl,
|
|
253
|
+
timeoutMs,
|
|
254
|
+
storyTimeout
|
|
255
|
+
);
|
|
256
|
+
lastError = void 0;
|
|
257
|
+
break;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
lastError = error;
|
|
260
|
+
if (!(error instanceof StoryTimeoutError) || attempts >= maxAttempts) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
console.warn(
|
|
264
|
+
`[screenshot] Retrying ${story.id} after timeout (attempt ${attempts + 1}/${maxAttempts})`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
completed++;
|
|
269
|
+
const absoluteIndex = offset + completed;
|
|
270
|
+
const durationMs = Date.now() - startedAt;
|
|
210
271
|
if (lightResult && darkResult) {
|
|
211
272
|
const fileHash = computeFileHash(story.importPath);
|
|
212
273
|
updateManifest(story.id, story.importPath, fileHash, lightResult.boundingBox);
|
|
274
|
+
console.log(
|
|
275
|
+
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`,
|
|
276
|
+
{ storyId: story.id, attempts, durationMs, outcome: "ok" }
|
|
277
|
+
);
|
|
278
|
+
} else if (lastError) {
|
|
279
|
+
console.error(
|
|
280
|
+
`[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
|
|
281
|
+
lastError instanceof Error ? lastError.message : lastError,
|
|
282
|
+
{
|
|
283
|
+
storyId: story.id,
|
|
284
|
+
attempts,
|
|
285
|
+
durationMs,
|
|
286
|
+
outcome: lastError instanceof StoryTimeoutError ? "timeout" : "error"
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
console.warn(`[${absoluteIndex}/${displayTotal}] Partial result ${story.id}`, {
|
|
291
|
+
storyId: story.id,
|
|
292
|
+
attempts,
|
|
293
|
+
durationMs,
|
|
294
|
+
outcome: "partial",
|
|
295
|
+
hasLight: !!lightResult,
|
|
296
|
+
hasDark: !!darkResult
|
|
297
|
+
});
|
|
213
298
|
}
|
|
214
|
-
completed++;
|
|
215
|
-
const absoluteIndex = offset + completed;
|
|
216
|
-
console.log(
|
|
217
|
-
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
|
|
218
|
-
);
|
|
219
299
|
})
|
|
220
300
|
);
|
|
221
301
|
}
|
|
@@ -260,10 +340,27 @@ function getStorybookCommand(storybookDir) {
|
|
|
260
340
|
console.log(
|
|
261
341
|
`\u{1F4E6} Detected package manager: ${pm}${repoRoot ? ` (from ${repoRoot})` : ""}`
|
|
262
342
|
);
|
|
263
|
-
|
|
343
|
+
let extraArgs = [];
|
|
344
|
+
try {
|
|
345
|
+
const pkgPath = resolve(storybookDir, "package.json");
|
|
346
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
347
|
+
const script = pkg.scripts?.storybook ?? "";
|
|
348
|
+
const hasPort = script.includes("-p ") || script.includes("--port");
|
|
349
|
+
const hasNoOpen = script.includes("--no-open");
|
|
350
|
+
if (!hasPort || !hasNoOpen) {
|
|
351
|
+
const needsSeparator = pm === "npm" || pm === "pnpm";
|
|
352
|
+
const flags = [];
|
|
353
|
+
if (!hasPort) flags.push("-p", "6006");
|
|
354
|
+
if (!hasNoOpen) flags.push("--no-open");
|
|
355
|
+
extraArgs = needsSeparator && flags.length > 0 ? ["--", ...flags] : flags;
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
|
|
359
|
+
extraArgs = [...separator, "-p", "6006", "--no-open"];
|
|
360
|
+
}
|
|
264
361
|
return {
|
|
265
362
|
command: pm,
|
|
266
|
-
args: ["run", "storybook", ...
|
|
363
|
+
args: ["run", "storybook", ...extraArgs]
|
|
267
364
|
};
|
|
268
365
|
}
|
|
269
366
|
async function isStorybookRunning(url) {
|
|
@@ -276,14 +373,38 @@ async function isStorybookRunning(url) {
|
|
|
276
373
|
return false;
|
|
277
374
|
}
|
|
278
375
|
}
|
|
279
|
-
async function
|
|
376
|
+
async function waitForStorybookReady(url, timeoutMs = 12e4) {
|
|
377
|
+
const start = Date.now();
|
|
378
|
+
const pollInterval = 2e3;
|
|
379
|
+
while (Date.now() - start < timeoutMs) {
|
|
380
|
+
if (await isStorybookRunning(url)) return;
|
|
381
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
382
|
+
}
|
|
383
|
+
throw new Error(`Storybook did not become ready within ${timeoutMs / 1e3}s`);
|
|
384
|
+
}
|
|
385
|
+
async function fetchStoryIndex(url, retries = 3) {
|
|
280
386
|
const indexUrl = `${url}/index.json`;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
387
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
388
|
+
try {
|
|
389
|
+
const response = await fetch(indexUrl, {
|
|
390
|
+
signal: AbortSignal.timeout(1e4)
|
|
391
|
+
});
|
|
392
|
+
if (!response.ok) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Failed to fetch story index: ${response.status} ${response.statusText}`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const data = await response.json();
|
|
398
|
+
return Object.values(data.entries);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (attempt === retries) throw error;
|
|
401
|
+
console.log(
|
|
402
|
+
`\u26A0\uFE0F Fetch story index failed (attempt ${attempt}/${retries}), retrying in 3s...`
|
|
403
|
+
);
|
|
404
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
405
|
+
}
|
|
284
406
|
}
|
|
285
|
-
|
|
286
|
-
return Object.values(data.entries);
|
|
407
|
+
throw new Error("Unreachable");
|
|
287
408
|
}
|
|
288
409
|
async function startStorybook(command2, args, storybookDir) {
|
|
289
410
|
console.log(
|
|
@@ -301,12 +422,12 @@ async function startStorybook(command2, args, storybookDir) {
|
|
|
301
422
|
const timeout = setTimeout(() => {
|
|
302
423
|
if (!started) {
|
|
303
424
|
storybookProcess.kill();
|
|
304
|
-
reject(new Error("Storybook failed to start within
|
|
425
|
+
reject(new Error("Storybook failed to start within 180 seconds"));
|
|
305
426
|
}
|
|
306
|
-
},
|
|
427
|
+
}, 18e4);
|
|
307
428
|
storybookProcess.stdout?.on("data", (data) => {
|
|
308
429
|
const output = data.toString();
|
|
309
|
-
if (output.includes("Local:") || output.includes("localhost:")) {
|
|
430
|
+
if (output.includes("Local:") || output.includes("localhost:") || output.includes("local:")) {
|
|
310
431
|
if (!started) {
|
|
311
432
|
started = true;
|
|
312
433
|
clearTimeout(timeout);
|
|
@@ -335,7 +456,7 @@ async function warmupStorybook(url, firstStoryId) {
|
|
|
335
456
|
try {
|
|
336
457
|
const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
|
|
337
458
|
await page.goto(warmupUrl, { timeout: 15e3 });
|
|
338
|
-
await page.waitForLoadState("
|
|
459
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 15e3 });
|
|
339
460
|
console.log("\u2705 Storybook warmed up");
|
|
340
461
|
} catch {
|
|
341
462
|
console.log("\u26A0\uFE0F Warmup had issues, proceeding anyway");
|
|
@@ -358,6 +479,8 @@ async function generateScreenshots(options = {}) {
|
|
|
358
479
|
const { command: command2, args } = getStorybookCommand(storybookDir);
|
|
359
480
|
storybookProcess = await startStorybook(command2, args, storybookDir);
|
|
360
481
|
weStartedStorybook = true;
|
|
482
|
+
console.log("\u23F3 Waiting for Storybook to finish compiling...");
|
|
483
|
+
await waitForStorybookReady(url);
|
|
361
484
|
}
|
|
362
485
|
const allStories = await fetchStoryIndex(url);
|
|
363
486
|
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 };
|