@onlook/storybook-plugin 0.3.2 → 0.3.3
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 +115 -40
- package/dist/index.js +30 -30
- package/dist/screenshot-service/index.js +59 -27
- package/package.json +1 -1
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;
|
|
@@ -199,23 +206,48 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
199
206
|
batch.map(async (story) => {
|
|
200
207
|
if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
|
|
201
208
|
completed++;
|
|
202
|
-
const
|
|
203
|
-
console.log(`[${
|
|
209
|
+
const absoluteIndex = offset + completed;
|
|
210
|
+
console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
|
|
204
211
|
return;
|
|
205
212
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
237
|
+
}
|
|
238
|
+
completed++;
|
|
239
|
+
const absoluteIndex = offset + completed;
|
|
240
|
+
console.log(
|
|
241
|
+
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
|
|
242
|
+
);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
completed++;
|
|
245
|
+
const absoluteIndex = offset + completed;
|
|
246
|
+
console.error(
|
|
247
|
+
`[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
|
|
248
|
+
error instanceof Error ? error.message : error
|
|
249
|
+
);
|
|
213
250
|
}
|
|
214
|
-
completed++;
|
|
215
|
-
const absoluteIndex = offset + completed;
|
|
216
|
-
console.log(
|
|
217
|
-
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
|
|
218
|
-
);
|
|
219
251
|
})
|
|
220
252
|
);
|
|
221
253
|
}
|
|
@@ -260,10 +292,27 @@ function getStorybookCommand(storybookDir) {
|
|
|
260
292
|
console.log(
|
|
261
293
|
`\u{1F4E6} Detected package manager: ${pm}${repoRoot ? ` (from ${repoRoot})` : ""}`
|
|
262
294
|
);
|
|
263
|
-
|
|
295
|
+
let extraArgs = [];
|
|
296
|
+
try {
|
|
297
|
+
const pkgPath = resolve(storybookDir, "package.json");
|
|
298
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
299
|
+
const script = pkg.scripts?.storybook ?? "";
|
|
300
|
+
const hasPort = script.includes("-p ") || script.includes("--port");
|
|
301
|
+
const hasNoOpen = script.includes("--no-open");
|
|
302
|
+
if (!hasPort || !hasNoOpen) {
|
|
303
|
+
const needsSeparator = pm === "npm" || pm === "pnpm";
|
|
304
|
+
const flags = [];
|
|
305
|
+
if (!hasPort) flags.push("-p", "6006");
|
|
306
|
+
if (!hasNoOpen) flags.push("--no-open");
|
|
307
|
+
extraArgs = needsSeparator && flags.length > 0 ? ["--", ...flags] : flags;
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
const separator = pm === "npm" || pm === "pnpm" ? ["--"] : [];
|
|
311
|
+
extraArgs = [...separator, "-p", "6006", "--no-open"];
|
|
312
|
+
}
|
|
264
313
|
return {
|
|
265
314
|
command: pm,
|
|
266
|
-
args: ["run", "storybook", ...
|
|
315
|
+
args: ["run", "storybook", ...extraArgs]
|
|
267
316
|
};
|
|
268
317
|
}
|
|
269
318
|
async function isStorybookRunning(url) {
|
|
@@ -276,14 +325,38 @@ async function isStorybookRunning(url) {
|
|
|
276
325
|
return false;
|
|
277
326
|
}
|
|
278
327
|
}
|
|
279
|
-
async function
|
|
328
|
+
async function waitForStorybookReady(url, timeoutMs = 12e4) {
|
|
329
|
+
const start = Date.now();
|
|
330
|
+
const pollInterval = 2e3;
|
|
331
|
+
while (Date.now() - start < timeoutMs) {
|
|
332
|
+
if (await isStorybookRunning(url)) return;
|
|
333
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
334
|
+
}
|
|
335
|
+
throw new Error(`Storybook did not become ready within ${timeoutMs / 1e3}s`);
|
|
336
|
+
}
|
|
337
|
+
async function fetchStoryIndex(url, retries = 3) {
|
|
280
338
|
const indexUrl = `${url}/index.json`;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
339
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetch(indexUrl, {
|
|
342
|
+
signal: AbortSignal.timeout(1e4)
|
|
343
|
+
});
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`Failed to fetch story index: ${response.status} ${response.statusText}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const data = await response.json();
|
|
350
|
+
return Object.values(data.entries);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (attempt === retries) throw error;
|
|
353
|
+
console.log(
|
|
354
|
+
`\u26A0\uFE0F Fetch story index failed (attempt ${attempt}/${retries}), retrying in 3s...`
|
|
355
|
+
);
|
|
356
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
357
|
+
}
|
|
284
358
|
}
|
|
285
|
-
|
|
286
|
-
return Object.values(data.entries);
|
|
359
|
+
throw new Error("Unreachable");
|
|
287
360
|
}
|
|
288
361
|
async function startStorybook(command2, args, storybookDir) {
|
|
289
362
|
console.log(
|
|
@@ -301,12 +374,12 @@ async function startStorybook(command2, args, storybookDir) {
|
|
|
301
374
|
const timeout = setTimeout(() => {
|
|
302
375
|
if (!started) {
|
|
303
376
|
storybookProcess.kill();
|
|
304
|
-
reject(new Error("Storybook failed to start within
|
|
377
|
+
reject(new Error("Storybook failed to start within 180 seconds"));
|
|
305
378
|
}
|
|
306
|
-
},
|
|
379
|
+
}, 18e4);
|
|
307
380
|
storybookProcess.stdout?.on("data", (data) => {
|
|
308
381
|
const output = data.toString();
|
|
309
|
-
if (output.includes("Local:") || output.includes("localhost:")) {
|
|
382
|
+
if (output.includes("Local:") || output.includes("localhost:") || output.includes("local:")) {
|
|
310
383
|
if (!started) {
|
|
311
384
|
started = true;
|
|
312
385
|
clearTimeout(timeout);
|
|
@@ -335,7 +408,7 @@ async function warmupStorybook(url, firstStoryId) {
|
|
|
335
408
|
try {
|
|
336
409
|
const warmupUrl = `${url}/iframe.html?id=${firstStoryId}&viewMode=story`;
|
|
337
410
|
await page.goto(warmupUrl, { timeout: 15e3 });
|
|
338
|
-
await page.waitForLoadState("
|
|
411
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 15e3 });
|
|
339
412
|
console.log("\u2705 Storybook warmed up");
|
|
340
413
|
} catch {
|
|
341
414
|
console.log("\u26A0\uFE0F Warmup had issues, proceeding anyway");
|
|
@@ -358,6 +431,8 @@ async function generateScreenshots(options = {}) {
|
|
|
358
431
|
const { command: command2, args } = getStorybookCommand(storybookDir);
|
|
359
432
|
storybookProcess = await startStorybook(command2, args, storybookDir);
|
|
360
433
|
weStartedStorybook = true;
|
|
434
|
+
console.log("\u23F3 Waiting for Storybook to finish compiling...");
|
|
435
|
+
await waitForStorybookReady(url);
|
|
361
436
|
}
|
|
362
437
|
const allStories = await fetchStoryIndex(url);
|
|
363
438
|
const total = allStories.length;
|
package/dist/index.js
CHANGED
|
@@ -142,20 +142,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
142
142
|
await page.goto(url, { timeout: timeoutMs });
|
|
143
143
|
await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
|
|
144
144
|
await page.waitForLoadState("load", { timeout: timeoutMs });
|
|
145
|
-
|
|
145
|
+
try {
|
|
146
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
146
149
|
await page.evaluate(() => document.fonts.ready);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
try {
|
|
151
|
+
await page.evaluate(async () => {
|
|
152
|
+
const images = document.querySelectorAll("img");
|
|
153
|
+
await Promise.all(
|
|
154
|
+
Array.from(images).map((img) => {
|
|
155
|
+
if (img.complete) return Promise.resolve();
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
img.addEventListener("load", resolve);
|
|
158
|
+
img.addEventListener("error", resolve);
|
|
159
|
+
setTimeout(resolve, 3e3);
|
|
160
|
+
});
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
159
166
|
const contentBounds = await page.evaluate(() => {
|
|
160
167
|
const root = document.querySelector("#storybook-root");
|
|
161
168
|
if (!root) return null;
|
|
@@ -331,7 +338,7 @@ var gitRoot = findGitRoot(storybookDir);
|
|
|
331
338
|
var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
|
|
332
339
|
var repoRoot = gitRoot || process.cwd();
|
|
333
340
|
var DEFAULT_ALLOWED_ORIGINS = [
|
|
334
|
-
"https://app.onlook.
|
|
341
|
+
"https://app.onlook.com",
|
|
335
342
|
"http://localhost:3000",
|
|
336
343
|
"http://localhost:6006"
|
|
337
344
|
];
|
|
@@ -496,17 +503,16 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
496
503
|
const mainPlugin = {
|
|
497
504
|
name: "storybook-onlook-plugin",
|
|
498
505
|
config() {
|
|
499
|
-
const isE2B = !!process.env.E2B_SANDBOX;
|
|
500
506
|
return {
|
|
501
507
|
server: {
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
508
|
+
// E2B sandbox HMR configuration
|
|
509
|
+
hmr: {
|
|
510
|
+
// E2B sandboxes use HTTPS, so we need secure WebSocket
|
|
511
|
+
protocol: "wss",
|
|
512
|
+
// E2B routes through standard HTTPS port 443
|
|
513
|
+
clientPort: 443,
|
|
514
|
+
// The actual Storybook server port inside the sandbox
|
|
515
|
+
port
|
|
510
516
|
},
|
|
511
517
|
cors: {
|
|
512
518
|
origin: allowedOrigins
|
|
@@ -528,13 +534,7 @@ function storybookOnlookPlugin(options = {}) {
|
|
|
528
534
|
console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
|
|
529
535
|
server.middlewares.use(serveMetadataAndScreenshots);
|
|
530
536
|
},
|
|
531
|
-
|
|
532
|
-
// consumes them. In local dev this blocks HMR with 36 Playwright
|
|
533
|
-
// page loads per save. Screenshots are still available on-demand
|
|
534
|
-
// via /api/capture-screenshot and the CLI.
|
|
535
|
-
...process.env.E2B_SANDBOX && {
|
|
536
|
-
handleHotUpdate: handleStoryFileChange
|
|
537
|
-
}
|
|
537
|
+
handleHotUpdate: handleStoryFileChange
|
|
538
538
|
};
|
|
539
539
|
return [componentLocPlugin(), mainPlugin];
|
|
540
540
|
}
|
|
@@ -93,20 +93,27 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
|
|
|
93
93
|
await page.goto(url, { timeout: timeoutMs });
|
|
94
94
|
await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
|
|
95
95
|
await page.waitForLoadState("load", { timeout: timeoutMs });
|
|
96
|
-
|
|
96
|
+
try {
|
|
97
|
+
await page.waitForLoadState("networkidle", { timeout: 5e3 });
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
97
100
|
await page.evaluate(() => document.fonts.ready);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
try {
|
|
102
|
+
await page.evaluate(async () => {
|
|
103
|
+
const images = document.querySelectorAll("img");
|
|
104
|
+
await Promise.all(
|
|
105
|
+
Array.from(images).map((img) => {
|
|
106
|
+
if (img.complete) return Promise.resolve();
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
img.addEventListener("load", resolve);
|
|
109
|
+
img.addEventListener("error", resolve);
|
|
110
|
+
setTimeout(resolve, 3e3);
|
|
111
|
+
});
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
110
117
|
const contentBounds = await page.evaluate(() => {
|
|
111
118
|
const root = document.querySelector("#storybook-root");
|
|
112
119
|
if (!root) return null;
|
|
@@ -197,23 +204,48 @@ async function generateAllScreenshots(stories, storybookUrl = "http://localhost:
|
|
|
197
204
|
batch.map(async (story) => {
|
|
198
205
|
if (skipExisting && screenshotExists(story.id, "light") && screenshotExists(story.id, "dark")) {
|
|
199
206
|
completed++;
|
|
200
|
-
const
|
|
201
|
-
console.log(`[${
|
|
207
|
+
const absoluteIndex = offset + completed;
|
|
208
|
+
console.log(`[${absoluteIndex}/${displayTotal}] Skipped (exists) ${story.id}`);
|
|
202
209
|
return;
|
|
203
210
|
}
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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);
|
|
235
|
+
}
|
|
236
|
+
completed++;
|
|
237
|
+
const absoluteIndex = offset + completed;
|
|
238
|
+
console.log(
|
|
239
|
+
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
completed++;
|
|
243
|
+
const absoluteIndex = offset + completed;
|
|
244
|
+
console.error(
|
|
245
|
+
`[${absoluteIndex}/${displayTotal}] \u26A0\uFE0F Failed ${story.id}:`,
|
|
246
|
+
error instanceof Error ? error.message : error
|
|
247
|
+
);
|
|
211
248
|
}
|
|
212
|
-
completed++;
|
|
213
|
-
const absoluteIndex = offset + completed;
|
|
214
|
-
console.log(
|
|
215
|
-
`[${absoluteIndex}/${displayTotal}] Generated screenshots for ${story.id}`
|
|
216
|
-
);
|
|
217
249
|
})
|
|
218
250
|
);
|
|
219
251
|
}
|