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

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.
@@ -0,0 +1,1458 @@
1
+ import { createRequire } from 'node:module';
2
+ import crypto, { createHash } from 'crypto';
3
+ import fs5, { existsSync } from 'fs';
4
+ import path7, { dirname, join, relative } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import generateModule from '@babel/generator';
7
+ import { parse } from '@babel/parser';
8
+ import traverseModule from '@babel/traverse';
9
+ import * as t from '@babel/types';
10
+ import { execFile } from 'child_process';
11
+ import { createRequire as createRequire$1 } from 'module';
12
+ import { promisify } from 'util';
13
+ import { chromium } from 'playwright';
14
+
15
+ globalThis.require = createRequire(import.meta.url);
16
+ function componentLocPlugin(options = {}) {
17
+ const include = options.include ?? /\.(jsx|tsx)$/;
18
+ const traverse = traverseModule.default ?? traverseModule;
19
+ const generate = generateModule.default ?? generateModule;
20
+ let root;
21
+ return {
22
+ name: "onbook-component-loc",
23
+ enforce: "pre",
24
+ apply: "serve",
25
+ configResolved(config) {
26
+ root = config.root;
27
+ },
28
+ transform(code, id) {
29
+ const filepath = id.split("?", 1)[0];
30
+ if (!filepath || filepath.includes("node_modules")) return null;
31
+ if (!include.test(filepath)) return null;
32
+ if (/\.stories\.(jsx?|tsx?)$/.test(filepath)) return null;
33
+ const ast = parse(code, {
34
+ sourceType: "module",
35
+ plugins: ["jsx", "typescript"],
36
+ sourceFilename: filepath
37
+ });
38
+ let mutated = false;
39
+ const relativePath = path7.relative(root, filepath);
40
+ traverse(ast, {
41
+ JSXElement(nodePath) {
42
+ const opening = nodePath.node.openingElement;
43
+ const element = nodePath.node;
44
+ if (!opening.loc || !element.loc) return;
45
+ const alreadyTagged = opening.attributes.some(
46
+ (attribute) => t.isJSXAttribute(attribute) && attribute.name.name === "data-component-file"
47
+ );
48
+ if (alreadyTagged) return;
49
+ opening.attributes.push(
50
+ t.jsxAttribute(
51
+ t.jsxIdentifier("data-component-file"),
52
+ t.stringLiteral(relativePath)
53
+ ),
54
+ t.jsxAttribute(
55
+ t.jsxIdentifier("data-component-start"),
56
+ t.stringLiteral(`${element.loc.start.line}:${element.loc.start.column}`)
57
+ ),
58
+ t.jsxAttribute(
59
+ t.jsxIdentifier("data-component-end"),
60
+ t.stringLiteral(`${element.loc.end.line}:${element.loc.end.column}`)
61
+ )
62
+ );
63
+ mutated = true;
64
+ }
65
+ });
66
+ if (!mutated) return null;
67
+ const output = generate(
68
+ ast,
69
+ { retainLines: true, sourceMaps: true, sourceFileName: id },
70
+ code
71
+ );
72
+ return { code: output.code, map: output.map };
73
+ }
74
+ };
75
+ }
76
+
77
+ // src/containment-contract/containment-contract.ts
78
+ var ONLOOK_CONTAINED_MODULES_GLOBAL = "__ONLOOK_CONTAINED_MODULES__";
79
+ var ONLOOK_CONTAINED_PROP = "__onlookContained";
80
+
81
+ // src/env-containment/env-containment.ts
82
+ var ENV_CONTAINMENT_PLUGIN_NAME = "onlook-env-containment";
83
+ var ENV_CONTAINMENT_VIRTUAL_PREFIX = "\0onlook-env-containment:";
84
+ var VALID_IDENT_RE = /^[A-Za-z_$][\w$]*$/;
85
+ var MATCH_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
86
+ function normalizeSlashes(p) {
87
+ return p.replace(/\\/g, "/");
88
+ }
89
+ function stripQuery(id) {
90
+ const q = id.indexOf("?");
91
+ return q === -1 ? id : id.slice(0, q);
92
+ }
93
+ function envContainmentPlugin(options) {
94
+ const substitutions = (options?.substitute ?? []).filter(
95
+ (s) => typeof s.module === "string" && s.module.length > 0
96
+ );
97
+ const defineEntries = options?.define ?? {};
98
+ if (substitutions.length === 0 && Object.keys(defineEntries).length === 0) {
99
+ return null;
100
+ }
101
+ const byModule = /* @__PURE__ */ new Map();
102
+ for (const sub of substitutions) {
103
+ byModule.set(normalizeSlashes(sub.module).replace(/^\.\//, ""), sub);
104
+ }
105
+ const targetBasenames = /* @__PURE__ */ new Set();
106
+ for (const rel of byModule.keys()) {
107
+ const base = rel.slice(rel.lastIndexOf("/") + 1);
108
+ targetBasenames.add(base.replace(/\.[^.]+$/, ""));
109
+ }
110
+ const specifierMightMatch = (cleaned) => {
111
+ const segment = cleaned.slice(cleaned.lastIndexOf("/") + 1);
112
+ return targetBasenames.has(segment.replace(/\.[^.]+$/, ""));
113
+ };
114
+ const resolveOutcomes = /* @__PURE__ */ new Map();
115
+ const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
116
+ const matchTarget = (absPath) => {
117
+ const norm = normalizeSlashes(path7.normalize(absPath));
118
+ for (const rel of byModule.keys()) {
119
+ for (const root of roots) {
120
+ const target = `${root}/${rel}`;
121
+ for (const suffix of MATCH_SUFFIXES) {
122
+ if (`${norm}${suffix}` === target) return rel;
123
+ }
124
+ }
125
+ }
126
+ return null;
127
+ };
128
+ return {
129
+ name: ENV_CONTAINMENT_PLUGIN_NAME,
130
+ // `pre` so we see specifiers before Vite's alias/resolve pipeline and
131
+ // before other resolveId hooks (e.g. tsconfig-paths) claim them.
132
+ enforce: "pre",
133
+ config() {
134
+ const keys = Object.entries(defineEntries);
135
+ if (keys.length === 0) return {};
136
+ const define = {};
137
+ for (const [key, value] of keys) {
138
+ const serialized = JSON.stringify(value);
139
+ define[`process.env.${key}`] = serialized;
140
+ define[`import.meta.env.${key}`] = serialized;
141
+ }
142
+ return { define };
143
+ },
144
+ configResolved(config) {
145
+ if (config.root) roots.add(normalizeSlashes(config.root));
146
+ },
147
+ async resolveId(source, importer) {
148
+ if (byModule.size === 0) return null;
149
+ if (source.startsWith("\0")) return null;
150
+ const cleaned = stripQuery(source);
151
+ let direct = null;
152
+ if (path7.isAbsolute(cleaned)) {
153
+ direct = cleaned;
154
+ } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
155
+ direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
156
+ }
157
+ if (direct) {
158
+ const rel = matchTarget(direct);
159
+ if (rel) return ENV_CONTAINMENT_VIRTUAL_PREFIX + rel;
160
+ return null;
161
+ }
162
+ if (!specifierMightMatch(cleaned)) return null;
163
+ if (typeof this.resolve !== "function") return null;
164
+ const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
165
+ const outcomeKey = `${importerDir}|${source}`;
166
+ const memoized = resolveOutcomes.get(outcomeKey);
167
+ if (memoized !== void 0) return memoized;
168
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
169
+ let outcome = null;
170
+ if (resolved?.id && !resolved.external) {
171
+ const rel = matchTarget(stripQuery(resolved.id));
172
+ if (rel) outcome = ENV_CONTAINMENT_VIRTUAL_PREFIX + rel;
173
+ }
174
+ resolveOutcomes.set(outcomeKey, outcome);
175
+ return outcome;
176
+ },
177
+ load(id) {
178
+ if (!id.startsWith(ENV_CONTAINMENT_VIRTUAL_PREFIX)) return null;
179
+ const rel = id.slice(ENV_CONTAINMENT_VIRTUAL_PREFIX.length);
180
+ const sub = byModule.get(rel);
181
+ if (!sub) return null;
182
+ return buildStandInSource(sub);
183
+ }
184
+ };
185
+ }
186
+ function buildStandInSource(sub) {
187
+ const marker = {
188
+ modulePath: sub.module,
189
+ reason: "env-validation",
190
+ envKeys: sub.envKeys ?? [],
191
+ ...sub.schemaKind ? { schemaKind: sub.schemaKind } : {}
192
+ };
193
+ const named = Array.from(
194
+ new Set(
195
+ (sub.exports ?? []).filter(
196
+ (name) => VALID_IDENT_RE.test(name) && name !== "default"
197
+ )
198
+ )
199
+ );
200
+ return [
201
+ "// Generated by @onlook/storybook-plugin (env containment).",
202
+ `// The real module (${sub.module}) validates environment variables at`,
203
+ "// module scope and throws on import when keys are missing. This stand-in",
204
+ "// defers the throw to property access so Storybook can boot and the",
205
+ "// Onlook error boundary can attribute the failure to the module.",
206
+ `const __onlookMarker = ${JSON.stringify(marker)};`,
207
+ "const __onlookGlobal = globalThis;",
208
+ `__onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL} =`,
209
+ ` __onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL} || {};`,
210
+ `__onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL}[__onlookMarker.modulePath] = __onlookMarker;`,
211
+ "function __onlookContainedError(access) {",
212
+ " const keys = __onlookMarker.envKeys.length",
213
+ " ? ' Missing env keys: ' + __onlookMarker.envKeys.join(', ') + '.'",
214
+ " : '';",
215
+ " const err = new Error(",
216
+ " '[onlook] ' + __onlookMarker.modulePath +",
217
+ " ' was contained: it validates environment variables at module scope' +",
218
+ " ' and the required keys are not set. Accessed: ' + access + '.' + keys,",
219
+ " );",
220
+ ` err.${ONLOOK_CONTAINED_PROP} = __onlookMarker;`,
221
+ " return err;",
222
+ "}",
223
+ "function __onlookStandIn(exportName) {",
224
+ " return new Proxy(function () {}, {",
225
+ " get(_target, prop) {",
226
+ ` if (prop === '${ONLOOK_CONTAINED_PROP}') return __onlookMarker;`,
227
+ " if (typeof prop === 'symbol' || prop === 'then') return undefined;",
228
+ " throw __onlookContainedError(exportName + '.' + String(prop));",
229
+ " },",
230
+ " apply() {",
231
+ " throw __onlookContainedError(exportName + '()');",
232
+ " },",
233
+ " construct() {",
234
+ " throw __onlookContainedError('new ' + exportName + '()');",
235
+ " },",
236
+ " });",
237
+ "}",
238
+ "export default __onlookStandIn('default');",
239
+ ...named.map(
240
+ (name) => `export const ${name} = __onlookStandIn(${JSON.stringify(name)});`
241
+ ),
242
+ ""
243
+ ].join("\n");
244
+ }
245
+
246
+ // src/fast-refresh-tolerant-exports/fast-refresh-tolerant-exports.ts
247
+ function fastRefreshTolerantExportsPlugin() {
248
+ const REGISTER_CALL_RE = /(\w+)\.registerExportsForReactRefresh\((["'])([^"'\n]+)\2,\s*([A-Za-z_$][\w$]*)\)/;
249
+ return {
250
+ name: "onbook-fast-refresh-tolerant-exports",
251
+ enforce: "post",
252
+ apply: "serve",
253
+ transform(code, id) {
254
+ if (!/\.(tsx|jsx)$/.test(id)) return null;
255
+ if (id.includes("node_modules")) return null;
256
+ const m = code.match(REGISTER_CALL_RE);
257
+ if (!m) return null;
258
+ const [fullMatch, , , idValue, exportsVar] = m;
259
+ if (!idValue || !exportsVar) return null;
260
+ const idLiteral = JSON.stringify(idValue);
261
+ const postRegister = `;(()=>{if(typeof window==='undefined')return;const M=(window.__onl1176_nonComponentExports||=Object.create(null));const isLikelyComponentType=(t)=>{switch(typeof t){case 'function':{if(t.prototype!=null){if(t.prototype.isReactComponent)return true;const o=Object.getOwnPropertyNames(t.prototype);if(o.length>1||o[0]!=='constructor')return false;if(Object.getPrototypeOf(t.prototype)!==Object.prototype)return false;}const n=t.name||t.displayName;return typeof n==='string'&&/^[A-Z]/.test(n);}case 'object':{if(t==null)return false;const s=t.$$typeof;return s===Symbol.for('react.forward_ref')||s===Symbol.for('react.memo');}default:return false;}};const isPlainObject=(o)=>Object.prototype.toString.call(o)==='[object Object]'&&(o.constructor===Object||o.constructor===undefined);const isCompoundComponent=(t)=>{if(!isPlainObject(t))return false;for(const k in t)if(!isLikelyComponentType(t[k]))return false;return true;};const isComp=(v)=>isLikelyComponentType(v)||isCompoundComponent(v);const ig=[];for(const k in ${exportsVar}){if(k==='__esModule')continue;if(!isComp(${exportsVar}[k]))ig.push(k);}M[` + idLiteral + "]=ig;if(!window.__getReactRefreshIgnoredExports){window.__getReactRefreshIgnoredExports=({id})=>window.__onl1176_nonComponentExports?.[id]||[];}})();";
262
+ const out = code.replace(fullMatch, fullMatch + postRegister);
263
+ return out === code ? null : { code: out, map: null };
264
+ }
265
+ };
266
+ }
267
+ var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
268
+ var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
269
+ var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
270
+ var VIEWPORT_WIDTH = 1920;
271
+ var VIEWPORT_HEIGHT = 1080;
272
+ var MIN_COMPONENT_WIDTH = 420;
273
+ var MIN_COMPONENT_HEIGHT = 280;
274
+
275
+ // src/utils/fileSystem/fileSystem.ts
276
+ function ensureCacheDirectories() {
277
+ if (!fs5.existsSync(CACHE_DIR)) {
278
+ fs5.mkdirSync(CACHE_DIR, { recursive: true });
279
+ }
280
+ if (!fs5.existsSync(SCREENSHOTS_DIR)) {
281
+ fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
282
+ }
283
+ }
284
+ function computeFileHash(filePath) {
285
+ if (!fs5.existsSync(filePath)) {
286
+ return "";
287
+ }
288
+ const content = fs5.readFileSync(filePath, "utf-8");
289
+ return crypto.createHash("sha256").update(content).digest("hex");
290
+ }
291
+ function loadManifest() {
292
+ if (fs5.existsSync(MANIFEST_PATH)) {
293
+ const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
294
+ return JSON.parse(content);
295
+ }
296
+ return { stories: {} };
297
+ }
298
+ function saveManifest(manifest) {
299
+ ensureCacheDirectories();
300
+ fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
301
+ }
302
+ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
303
+ const manifest = loadManifest();
304
+ manifest.stories[storyId] = {
305
+ fileHash,
306
+ lastGenerated: (/* @__PURE__ */ new Date()).toISOString(),
307
+ sourcePath,
308
+ screenshots: {
309
+ light: `screenshots/${storyId}/light.png`,
310
+ dark: `screenshots/${storyId}/dark.png`
311
+ },
312
+ boundingBox: boundingBox ?? void 0
313
+ };
314
+ saveManifest(manifest);
315
+ }
316
+ var execFileAsync = promisify(execFile);
317
+ var require2 = createRequire$1(import.meta.url);
318
+ var browserPromise = null;
319
+ var installPromise = null;
320
+ var isMissingExecutableError = (err) => {
321
+ const msg = err instanceof Error ? err.message : String(err);
322
+ 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.
323
+ /chromium-\d+\/chrome-linux\/chrome/i.test(msg);
324
+ };
325
+ function resolvePlaywrightCli() {
326
+ const pkgJson = require2.resolve("playwright-core/package.json");
327
+ return join(dirname(pkgJson), "cli.js");
328
+ }
329
+ async function installChromium() {
330
+ if (!installPromise) {
331
+ installPromise = (async () => {
332
+ const cliPath = resolvePlaywrightCli();
333
+ console.log(
334
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
335
+ );
336
+ try {
337
+ const { stdout, stderr } = await execFileAsync(
338
+ process.execPath,
339
+ [cliPath, "install", "--force", "chromium"],
340
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
341
+ );
342
+ if (stdout)
343
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
344
+ if (stderr)
345
+ console.log(
346
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
347
+ );
348
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
349
+ } catch (err) {
350
+ const detail = err instanceof Error ? err.message : String(err);
351
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
352
+ throw new Error(`Playwright chromium install failed: ${detail}`);
353
+ } finally {
354
+ installPromise = null;
355
+ }
356
+ })();
357
+ }
358
+ return installPromise;
359
+ }
360
+ async function launchWithSelfHeal() {
361
+ try {
362
+ return await chromium.launch({ headless: true });
363
+ } catch (err) {
364
+ if (!isMissingExecutableError(err)) throw err;
365
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
366
+ await installChromium();
367
+ try {
368
+ return await chromium.launch({ headless: true });
369
+ } catch (postInstallErr) {
370
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
371
+ console.error(
372
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
373
+ );
374
+ throw new Error(
375
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
376
+ );
377
+ }
378
+ }
379
+ }
380
+ async function getBrowser() {
381
+ if (!browserPromise) {
382
+ browserPromise = launchWithSelfHeal().catch((err) => {
383
+ browserPromise = null;
384
+ throw err;
385
+ });
386
+ }
387
+ return browserPromise;
388
+ }
389
+ function getScreenshotPath(storyId, theme) {
390
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
391
+ return path7.join(storyDir, `${theme}.png`);
392
+ }
393
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
394
+ const browser = await getBrowser();
395
+ const context = await browser.newContext({
396
+ viewport: { width, height },
397
+ deviceScaleFactor: 2
398
+ });
399
+ const page = await context.newPage();
400
+ try {
401
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
402
+ await page.goto(url, { timeout: timeoutMs });
403
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
404
+ await page.waitForLoadState("load", { timeout: timeoutMs });
405
+ try {
406
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
407
+ } catch {
408
+ }
409
+ await page.evaluate(() => document.fonts.ready);
410
+ try {
411
+ await page.evaluate(async () => {
412
+ const images = document.querySelectorAll("img");
413
+ await Promise.all(
414
+ Array.from(images).map((img) => {
415
+ if (img.complete) return Promise.resolve();
416
+ return new Promise((resolve) => {
417
+ img.addEventListener("load", resolve);
418
+ img.addEventListener("error", resolve);
419
+ setTimeout(resolve, 3e3);
420
+ });
421
+ })
422
+ );
423
+ });
424
+ } catch {
425
+ }
426
+ const contentBounds = await page.evaluate(() => {
427
+ const root = document.querySelector("#storybook-root");
428
+ if (!root) return null;
429
+ const children = root.querySelectorAll("*");
430
+ if (children.length === 0) return null;
431
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
432
+ children.forEach((child) => {
433
+ const rect = child.getBoundingClientRect();
434
+ if (rect.width === 0 || rect.height === 0) return;
435
+ minX = Math.min(minX, rect.left);
436
+ minY = Math.min(minY, rect.top);
437
+ maxX = Math.max(maxX, rect.right);
438
+ maxY = Math.max(maxY, rect.bottom);
439
+ });
440
+ if (minX === Infinity) return null;
441
+ return {
442
+ x: minX,
443
+ y: minY,
444
+ width: maxX - minX,
445
+ height: maxY - minY
446
+ };
447
+ });
448
+ let screenshotBuffer;
449
+ let resultBoundingBox = null;
450
+ if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
451
+ const PADDING = 20;
452
+ const clippedWidth = Math.min(width, contentBounds.width + PADDING);
453
+ const clippedHeight = Math.min(height, contentBounds.height + PADDING);
454
+ resultBoundingBox = {
455
+ width: Math.max(MIN_COMPONENT_WIDTH, Math.round(clippedWidth)),
456
+ height: Math.max(MIN_COMPONENT_HEIGHT, Math.round(clippedHeight))
457
+ };
458
+ screenshotBuffer = await page.screenshot({
459
+ type: "png",
460
+ clip: {
461
+ x: Math.max(0, contentBounds.x - 10),
462
+ y: Math.max(0, contentBounds.y - 10),
463
+ width: clippedWidth,
464
+ height: clippedHeight
465
+ }
466
+ });
467
+ } else {
468
+ screenshotBuffer = await page.screenshot({ type: "png" });
469
+ }
470
+ return { buffer: screenshotBuffer, boundingBox: resultBoundingBox };
471
+ } finally {
472
+ await context.close();
473
+ }
474
+ }
475
+ async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
476
+ try {
477
+ ensureCacheDirectories();
478
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
479
+ if (!fs5.existsSync(storyDir)) {
480
+ fs5.mkdirSync(storyDir, { recursive: true });
481
+ }
482
+ const screenshotPath = getScreenshotPath(storyId, theme);
483
+ const { buffer, boundingBox } = await captureScreenshotBuffer(
484
+ storyId,
485
+ theme,
486
+ VIEWPORT_WIDTH,
487
+ VIEWPORT_HEIGHT,
488
+ storybookUrl,
489
+ timeoutMs
490
+ );
491
+ fs5.writeFileSync(screenshotPath, buffer);
492
+ return { path: screenshotPath, boundingBox };
493
+ } catch (error) {
494
+ console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
495
+ return null;
496
+ }
497
+ }
498
+
499
+ // src/screenshot-service/screenshot-service.ts
500
+ process.env.ONBOOK_PROXY_TOKEN ?? null;
501
+ process.env.ONBOOK_BROADCAST_URL ?? null;
502
+
503
+ // src/utils/notifyOnbookIndexChanged/notifyOnbookIndexChanged.ts
504
+ var DEBOUNCE_MS = 300;
505
+ var REQUEST_TIMEOUT_MS = 5e3;
506
+ var pending = null;
507
+ var inFlight = false;
508
+ var pendingTrailingCall = false;
509
+ function notifyOnbookIndexChanged() {
510
+ const proxyToken = process.env.ONBOOK_PROXY_TOKEN;
511
+ const broadcastUrl = process.env.ONBOOK_BROADCAST_URL;
512
+ if (!proxyToken || !broadcastUrl) return;
513
+ if (pending) clearTimeout(pending);
514
+ pending = setTimeout(() => {
515
+ pending = null;
516
+ if (inFlight) {
517
+ pendingTrailingCall = true;
518
+ return;
519
+ }
520
+ void fire(proxyToken, broadcastUrl);
521
+ }, DEBOUNCE_MS);
522
+ }
523
+ async function fire(proxyToken, broadcastUrl) {
524
+ inFlight = true;
525
+ try {
526
+ const res = await fetch(broadcastUrl, {
527
+ method: "POST",
528
+ headers: {
529
+ Authorization: `Bearer ${proxyToken}`,
530
+ "Content-Type": "application/json"
531
+ },
532
+ body: JSON.stringify({ event: { type: "STORYBOOK_INDEX_CHANGED" } }),
533
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
534
+ });
535
+ if (!res.ok) {
536
+ console.warn("[STORYBOOK_PLUGIN] broadcast non-2xx", {
537
+ status: res.status,
538
+ statusText: res.statusText
539
+ });
540
+ }
541
+ } catch (err) {
542
+ console.warn(
543
+ "[STORYBOOK_PLUGIN] index-changed broadcast failed",
544
+ err instanceof Error ? err.message : String(err)
545
+ );
546
+ } finally {
547
+ inFlight = false;
548
+ if (pendingTrailingCall) {
549
+ pendingTrailingCall = false;
550
+ notifyOnbookIndexChanged();
551
+ }
552
+ }
553
+ }
554
+
555
+ // src/utils/notifyOnbookIndexChanged/waitForIndexAndBroadcast.ts
556
+ var PROBE_INTERVAL_MS = 500;
557
+ var PROBE_CEILING_MS = 5 * 60 * 1e3;
558
+ async function waitForIndexAndBroadcast(port = 6006) {
559
+ const url = `http://localhost:${port}/onbook-index.json`;
560
+ const deadline = Date.now() + PROBE_CEILING_MS;
561
+ while (Date.now() < deadline) {
562
+ try {
563
+ const res = await fetch(url, {
564
+ signal: AbortSignal.timeout(2e3)
565
+ });
566
+ if (res.ok) {
567
+ notifyOnbookIndexChanged();
568
+ return;
569
+ }
570
+ } catch {
571
+ }
572
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
573
+ }
574
+ console.warn("[STORYBOOK_PLUGIN] waitForIndexAndBroadcast hit ceiling without a 2xx", {
575
+ url,
576
+ ceilingMs: PROBE_CEILING_MS
577
+ });
578
+ }
579
+
580
+ // src/handlers/handleStoryFileChange/handleStoryFileChange.ts
581
+ var cachedIndex = null;
582
+ var indexFetchPromise = null;
583
+ var pendingFiles = /* @__PURE__ */ new Set();
584
+ var debounceTimer = null;
585
+ var DEBOUNCE_MS2 = 500;
586
+ async function fetchStorybookIndex() {
587
+ try {
588
+ const response = await fetch("http://localhost:6006/index.json");
589
+ if (response.ok) {
590
+ cachedIndex = await response.json();
591
+ console.log("[Screenshots] Cached Storybook index");
592
+ }
593
+ } catch (error) {
594
+ console.error("[Screenshots] Failed to fetch Storybook index:", error);
595
+ }
596
+ }
597
+ function getStoriesForFile(filePath) {
598
+ if (!cachedIndex) return [];
599
+ const fileName = path7.basename(filePath);
600
+ return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
601
+ }
602
+ async function regenerateScreenshotsForFiles(files) {
603
+ await fetchStorybookIndex();
604
+ const allStoryIds = /* @__PURE__ */ new Set();
605
+ const fileToStories = /* @__PURE__ */ new Map();
606
+ for (const file of files) {
607
+ const storyIds = getStoriesForFile(file);
608
+ fileToStories.set(file, storyIds);
609
+ for (const id of storyIds) {
610
+ allStoryIds.add(id);
611
+ }
612
+ }
613
+ if (allStoryIds.size === 0) {
614
+ console.log("[Screenshots] No stories found for changed files");
615
+ return;
616
+ }
617
+ console.log(
618
+ `[Screenshots] Regenerating ${allStoryIds.size} stories from ${files.length} files`
619
+ );
620
+ const storybookUrl = "http://localhost:6006";
621
+ const storyBoundingBoxes = /* @__PURE__ */ new Map();
622
+ await Promise.all(
623
+ Array.from(allStoryIds).map(async (storyId) => {
624
+ const [lightResult, _darkResult] = await Promise.all([
625
+ generateScreenshot(storyId, "light", storybookUrl),
626
+ generateScreenshot(storyId, "dark", storybookUrl)
627
+ ]);
628
+ if (lightResult) {
629
+ storyBoundingBoxes.set(storyId, lightResult.boundingBox);
630
+ }
631
+ })
632
+ );
633
+ for (const [file, storyIds] of fileToStories) {
634
+ const fileHash = computeFileHash(file);
635
+ storyIds.forEach((storyId) => {
636
+ updateManifest(storyId, file, fileHash, storyBoundingBoxes.get(storyId));
637
+ });
638
+ }
639
+ console.log(`[Screenshots] \u2713 Regenerated ${allStoryIds.size} stories`);
640
+ }
641
+ function handleStoryFileChange({ file, modules }) {
642
+ if (file.endsWith(".stories.tsx") || file.endsWith(".stories.ts")) {
643
+ console.log(`[Screenshots] Story file changed: ${file}`);
644
+ notifyOnbookIndexChanged();
645
+ if (!cachedIndex && !indexFetchPromise) {
646
+ indexFetchPromise = fetchStorybookIndex();
647
+ }
648
+ pendingFiles.add(file);
649
+ if (debounceTimer) {
650
+ clearTimeout(debounceTimer);
651
+ }
652
+ debounceTimer = setTimeout(async () => {
653
+ const files = Array.from(pendingFiles);
654
+ pendingFiles.clear();
655
+ debounceTimer = null;
656
+ try {
657
+ await regenerateScreenshotsForFiles(files);
658
+ } catch (error) {
659
+ console.error("[Screenshots] Error regenerating screenshots:", error);
660
+ }
661
+ }, DEBOUNCE_MS2);
662
+ return modules;
663
+ }
664
+ }
665
+
666
+ // src/module-load-catch-all/module-load-catch-all.ts
667
+ var STORYBOOK_VIRTUAL_STORIES_ID = "virtual:/@storybook/builder-vite/storybook-stories.js";
668
+ var RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID = `\0${STORYBOOK_VIRTUAL_STORIES_ID}`;
669
+ var MODULE_LOAD_CATCH_ALL_PLUGIN_NAME = "onlook-module-load-catch-all";
670
+ var IMPORT_FN_CALL = "return await importers[path]();";
671
+ var INJECTED_BINDING = "__onlookSyntheticCsfModule";
672
+ async function onlookSyntheticCsfModule(importPath, loadError, containedProp) {
673
+ const original = loadError instanceof Error ? loadError : new Error(String(loadError));
674
+ const synthetic = new Error(
675
+ `Story module failed to load: ${importPath}
676
+ ${original.message}`
677
+ );
678
+ const marker = original[containedProp];
679
+ if (marker !== void 0) {
680
+ synthetic[containedProp] = marker;
681
+ }
682
+ synthetic.cause = original;
683
+ try {
684
+ console.error(
685
+ "[ONLOOK] Story module failed to load \u2014 serving contained synthetic stories.",
686
+ { importPath, error: original }
687
+ );
688
+ } catch {
689
+ }
690
+ const stories = [];
691
+ try {
692
+ const response = await fetch("./index.json");
693
+ if (response?.ok) {
694
+ const index = await response.json();
695
+ for (const entry of Object.values(index.entries ?? {})) {
696
+ if (!entry || entry.type !== "story") continue;
697
+ if (entry.importPath !== importPath) continue;
698
+ if (typeof entry.id !== "string") continue;
699
+ stories.push({
700
+ id: entry.id,
701
+ name: typeof entry.name === "string" ? entry.name : entry.id
702
+ });
703
+ }
704
+ }
705
+ } catch {
706
+ }
707
+ if (stories.length === 0) {
708
+ try {
709
+ const search = typeof window !== "undefined" && window.location ? window.location.search : "";
710
+ const urlId = new URLSearchParams(search).get("id");
711
+ if (urlId) stories.push({ id: urlId, name: "Needs setup" });
712
+ } catch {
713
+ }
714
+ }
715
+ const moduleExports = { default: {} };
716
+ const render = () => {
717
+ throw synthetic;
718
+ };
719
+ if (stories.length === 0) {
720
+ moduleExports.OnlookContained = { name: "Needs setup", render };
721
+ return moduleExports;
722
+ }
723
+ let i = 0;
724
+ for (const story of stories) {
725
+ moduleExports[`OnlookContained${i}`] = {
726
+ name: story.name,
727
+ // Forces the exact index story id — export names can't be recovered
728
+ // from a module that never evaluated.
729
+ parameters: { __id: story.id },
730
+ render
731
+ };
732
+ i += 1;
733
+ }
734
+ return moduleExports;
735
+ }
736
+ function wrapImportFn(code) {
737
+ if (code.includes(INJECTED_BINDING)) return null;
738
+ if (!code.includes(IMPORT_FN_CALL)) return null;
739
+ const wrappedCall = [
740
+ "try {",
741
+ " return await importers[path]();",
742
+ " } catch (__onlookLoadError) {",
743
+ ` return await ${INJECTED_BINDING}(path, __onlookLoadError, ${JSON.stringify(ONLOOK_CONTAINED_PROP)});`,
744
+ " }"
745
+ ].join("\n");
746
+ const body = code.replace(IMPORT_FN_CALL, wrappedCall);
747
+ return [
748
+ body,
749
+ "",
750
+ "// Injected by @onlook/storybook-plugin (module-load catch-all, U7).",
751
+ `const ${INJECTED_BINDING} = ${onlookSyntheticCsfModule.toString()};`,
752
+ ""
753
+ ].join("\n");
754
+ }
755
+ function moduleLoadCatchAllPlugin() {
756
+ let warnedDrift = false;
757
+ return {
758
+ name: MODULE_LOAD_CATCH_ALL_PLUGIN_NAME,
759
+ // 'pre' so the wrap sees builder-vite's raw codegen (served from its
760
+ // `load` hook) before normal-phase transforms (e.g. plugin-react, whose
761
+ // default filter matches the virtual id's `.js` suffix) reshape it.
762
+ enforce: "pre",
763
+ transform(code, id) {
764
+ if (id !== RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID && id !== STORYBOOK_VIRTUAL_STORIES_ID) {
765
+ return null;
766
+ }
767
+ const wrapped = wrapImportFn(code);
768
+ if (wrapped === null) {
769
+ if (!warnedDrift && !code.includes(INJECTED_BINDING)) {
770
+ warnedDrift = true;
771
+ console.warn(
772
+ "[STORYBOOK_PLUGIN] module-load-catch-all: unrecognized importFn codegen \u2014 passing through (module-load failures will surface as raw Storybook overlays)."
773
+ );
774
+ }
775
+ return null;
776
+ }
777
+ return { code: wrapped, map: null };
778
+ }
779
+ };
780
+ }
781
+
782
+ // src/screenshot-service/utils/console-logs/console-logs.ts
783
+ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
784
+ const browser = await getBrowser();
785
+ const context = await browser.newContext({
786
+ viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }
787
+ });
788
+ const page = await context.newPage();
789
+ const logs = [];
790
+ const pageErrors = [];
791
+ page.on("console", (msg) => {
792
+ const level = msg.type();
793
+ logs.push({
794
+ level: level === "warning" ? "warn" : level,
795
+ text: msg.text(),
796
+ timestamp: Date.now()
797
+ });
798
+ });
799
+ page.on("pageerror", (err) => {
800
+ pageErrors.push(err.message);
801
+ });
802
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
803
+ try {
804
+ await page.goto(url, { timeout: timeoutMs });
805
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
806
+ await page.waitForLoadState("load", { timeout: timeoutMs });
807
+ try {
808
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
809
+ } catch {
810
+ }
811
+ await new Promise((r) => setTimeout(r, 1e3));
812
+ return { logs, pageErrors, url };
813
+ } finally {
814
+ await context.close();
815
+ }
816
+ }
817
+
818
+ // src/soft-story-rerender/soft-story-rerender.ts
819
+ function softStoryRerenderPlugin() {
820
+ let warnedOnce = false;
821
+ return {
822
+ name: "onbook-soft-story-rerender",
823
+ enforce: "post",
824
+ apply: "serve",
825
+ transform(code, _id) {
826
+ if (!code.includes("__STORYBOOK_PREVIEW__.onStoriesChanged")) return null;
827
+ const pattern = /import\.meta\.hot\.accept\((['"`])([^'"`\n]+)\1,\s*(?:async\s+)?\(?\s*newModule\s*\)?\s*=>\s*\{[\s\S]*?window\.__STORYBOOK_PREVIEW__\.onStoriesChanged\(\{\s*importFn:\s*newModule\.importFn\s*\}\);?\s*\}\);/;
828
+ if (!pattern.test(code)) {
829
+ if (!warnedOnce) {
830
+ warnedOnce = true;
831
+ console.warn(
832
+ "[onbook soft-story-rerender] preview bootstrap matched the file but the stories-accept-handler pattern did not \u2014 soft rerender disabled for this build of @storybook/builder-vite. Story edits will still work, just with the default Storybook flash."
833
+ );
834
+ }
835
+ return null;
836
+ }
837
+ const replacement = `import.meta.hot.accept($QUOTE$$PATH$$QUOTE$, async (newModule) => {
838
+ // ONL-1176 soft rerender \u2014 call rerender() on each active StoryRender
839
+ // instead of preview.onStoriesChanged() (which tears down + remounts).
840
+ // This accept handler fires in EVERY canvas iframe whenever ANY story
841
+ // file changes (every iframe's preview bundle imports the same virtual
842
+ // stories index). We identity-check each render's unboundStoryFn so
843
+ // iframes whose underlying story file didn't change stay perfectly
844
+ // still \u2014 no 1-frame no-op tick.
845
+ const preview = window.__STORYBOOK_PREVIEW__;
846
+ if (!preview || !preview.storyStoreValue) {
847
+ window.__STORYBOOK_PREVIEW__?.onStoriesChanged({ importFn: newModule.importFn });
848
+ return;
849
+ }
850
+ try {
851
+ await preview.storyStoreValue.onStoriesChanged({ importFn: newModule.importFn });
852
+ const renders = preview.storyRenders || [];
853
+ if (renders.length === 0) {
854
+ preview.onStoriesChanged({ importFn: newModule.importFn });
855
+ return;
856
+ }
857
+ await Promise.all(
858
+ renders.map(async (r) => {
859
+ try {
860
+ const nextStory = await preview.storyStoreValue.loadStory({ storyId: r.id });
861
+ if (!nextStory) return;
862
+ const prev = r.story;
863
+ const unchanged =
864
+ prev &&
865
+ prev.unboundStoryFn === nextStory.unboundStoryFn &&
866
+ prev.storyFn === nextStory.storyFn &&
867
+ prev.moduleExport === nextStory.moduleExport &&
868
+ prev.playFunction === nextStory.playFunction;
869
+ if (unchanged) return;
870
+ r.story = nextStory;
871
+ // Stories using the \`mount\` API can't be rerendered in place
872
+ // (their play function owns mounting), so fall back to remount.
873
+ if (nextStory.usesMount && r.remount) {
874
+ return r.remount();
875
+ }
876
+ return r.rerender();
877
+ } catch (innerErr) {
878
+ console.warn('[onbook] rerender failed for story', r.id, innerErr);
879
+ }
880
+ }),
881
+ );
882
+ } catch (err) {
883
+ console.warn('[onbook] soft rerender failed, falling back', err);
884
+ preview.onStoriesChanged({ importFn: newModule.importFn });
885
+ }
886
+ });`;
887
+ const out = code.replace(pattern, (_match, quote, path8) => {
888
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
889
+ });
890
+ return out === code ? null : { code: out, map: null };
891
+ }
892
+ };
893
+ }
894
+ var TSCONFIG_CANDIDATES = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json"];
895
+ function resolveTsconfigAliases(root) {
896
+ try {
897
+ const config = readTsconfigAliasConfig(root);
898
+ if (!config) return [];
899
+ const base = path7.resolve(root, config.baseUrl ?? ".");
900
+ const aliases = [];
901
+ for (const [key, targets] of Object.entries(config.paths)) {
902
+ const target = targets[0];
903
+ if (!target) continue;
904
+ if (key.endsWith("/*") && target.endsWith("/*")) {
905
+ const findPrefix = key.slice(0, -1);
906
+ const targetDir = path7.resolve(base, target.slice(0, -2));
907
+ aliases.push({
908
+ find: new RegExp(`^${escapeRegExp(findPrefix)}`),
909
+ replacement: `${targetDir}/`
910
+ });
911
+ } else if (!key.includes("*") && !target.includes("*")) {
912
+ aliases.push({
913
+ find: new RegExp(`^${escapeRegExp(key)}$`),
914
+ replacement: path7.resolve(base, target)
915
+ });
916
+ }
917
+ }
918
+ return aliases.sort((a, b) => b.find.source.length - a.find.source.length);
919
+ } catch {
920
+ return [];
921
+ }
922
+ }
923
+ function readTsconfigAliasConfig(root) {
924
+ for (const candidate of TSCONFIG_CANDIDATES) {
925
+ const abs = path7.join(root, candidate);
926
+ let text;
927
+ try {
928
+ text = fs5.readFileSync(abs, "utf-8");
929
+ } catch {
930
+ continue;
931
+ }
932
+ let parsed;
933
+ try {
934
+ parsed = JSON.parse(stripJsonComments(text));
935
+ } catch {
936
+ continue;
937
+ }
938
+ const paths = parsed.compilerOptions?.paths;
939
+ if (paths && typeof paths === "object" && Object.keys(paths).length > 0) {
940
+ const baseUrl = parsed.compilerOptions?.baseUrl;
941
+ return typeof baseUrl === "string" ? { baseUrl, paths } : { paths };
942
+ }
943
+ }
944
+ return null;
945
+ }
946
+ function escapeRegExp(value) {
947
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
948
+ }
949
+ function stripJsonComments(text) {
950
+ let out = "";
951
+ let inString = false;
952
+ let inLine = false;
953
+ let inBlock = false;
954
+ for (let i = 0; i < text.length; i++) {
955
+ const c = text.charAt(i);
956
+ const next = text.charAt(i + 1);
957
+ if (inLine) {
958
+ if (c === "\n") {
959
+ inLine = false;
960
+ out += c;
961
+ }
962
+ continue;
963
+ }
964
+ if (inBlock) {
965
+ if (c === "*" && next === "/") {
966
+ inBlock = false;
967
+ i++;
968
+ }
969
+ continue;
970
+ }
971
+ if (inString) {
972
+ out += c;
973
+ if (c === "\\") {
974
+ out += next;
975
+ i++;
976
+ } else if (c === '"') {
977
+ inString = false;
978
+ }
979
+ continue;
980
+ }
981
+ if (c === '"') {
982
+ inString = true;
983
+ out += c;
984
+ } else if (c === "/" && next === "/") {
985
+ inLine = true;
986
+ i++;
987
+ } else if (c === "/" && next === "*") {
988
+ inBlock = true;
989
+ i++;
990
+ } else {
991
+ out += c;
992
+ }
993
+ }
994
+ return out;
995
+ }
996
+ function findGitRoot(startPath) {
997
+ let currentPath = startPath;
998
+ while (currentPath !== dirname(currentPath)) {
999
+ if (existsSync(join(currentPath, ".git"))) {
1000
+ return currentPath;
1001
+ }
1002
+ currentPath = dirname(currentPath);
1003
+ }
1004
+ return null;
1005
+ }
1006
+
1007
+ // src/storybook-onlook-plugin.ts
1008
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1009
+ var storybookDir = join(__dirname$1, "..");
1010
+ var gitRoot = findGitRoot(storybookDir);
1011
+ var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
1012
+ var repoRoot = gitRoot || process.cwd();
1013
+ var DEFAULT_ALLOWED_ORIGINS = [
1014
+ "https://app.onlook.com",
1015
+ "http://localhost:3000",
1016
+ "http://localhost:6006"
1017
+ ];
1018
+ var manifestCache = null;
1019
+ var manifestHash = null;
1020
+ function readManifestFromDisk(filePath) {
1021
+ try {
1022
+ if (!fs5.existsSync(filePath)) return null;
1023
+ const raw = fs5.readFileSync(filePath, "utf-8");
1024
+ const hash = createHash("sha1").update(raw).digest("hex");
1025
+ const manifest = JSON.parse(raw);
1026
+ return { manifest, hash };
1027
+ } catch (err) {
1028
+ console.warn("[STORYBOOK_PLUGIN] Failed to read manifest", {
1029
+ filePath,
1030
+ error: err instanceof Error ? err.message : String(err)
1031
+ });
1032
+ return null;
1033
+ }
1034
+ }
1035
+ function refreshManifest(filePath) {
1036
+ const result = readManifestFromDisk(filePath);
1037
+ if (!result) {
1038
+ return false;
1039
+ }
1040
+ if (result.hash === manifestHash) return false;
1041
+ manifestCache = result.manifest;
1042
+ manifestHash = result.hash;
1043
+ console.log("[STORYBOOK_PLUGIN] Manifest cache refreshed", {
1044
+ filePath,
1045
+ hash: result.hash.slice(0, 8),
1046
+ storyCount: Object.keys(result.manifest.stories ?? {}).length
1047
+ });
1048
+ return true;
1049
+ }
1050
+ var TAILWIND_ENTRY_CANDIDATES = [
1051
+ "src/app/globals.css",
1052
+ "app/globals.css",
1053
+ "src/index.css",
1054
+ "src/main.css",
1055
+ "src/styles.css",
1056
+ "src/styles/globals.css",
1057
+ "src/styles/index.css",
1058
+ "styles/globals.css",
1059
+ "styles/index.css",
1060
+ "app.css",
1061
+ "index.css"
1062
+ ];
1063
+ var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
1064
+ function detectTailwindEntryCss(cwd) {
1065
+ for (const rel of TAILWIND_ENTRY_CANDIDATES) {
1066
+ const abs = path7.join(cwd, rel);
1067
+ try {
1068
+ if (!fs5.existsSync(abs)) continue;
1069
+ const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
1070
+ if (TAILWIND_DIRECTIVE_PATTERN.test(head)) return abs;
1071
+ } catch {
1072
+ }
1073
+ }
1074
+ return null;
1075
+ }
1076
+ var serveMetadataAndScreenshots = (req, res, next) => {
1077
+ if (req.url === "/onbook-index.json") {
1078
+ console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
1079
+ const cacheBuster = Date.now();
1080
+ console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
1081
+ fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
1082
+ cache: "no-store",
1083
+ headers: {
1084
+ "Cache-Control": "no-cache",
1085
+ Pragma: "no-cache"
1086
+ }
1087
+ }).then((response) => {
1088
+ console.log("[STORYBOOK_PLUGIN] Storybook index fetch response", {
1089
+ status: response.status,
1090
+ ok: response.ok,
1091
+ statusText: response.statusText
1092
+ });
1093
+ return response.json();
1094
+ }).then((indexData) => {
1095
+ const manifest = manifestCache ?? { stories: {} };
1096
+ const defaultBoundingBox = { width: 1920, height: 1080 };
1097
+ for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
1098
+ const manifestEntry = manifest.stories?.[storyId];
1099
+ entry.boundingBox = manifestEntry?.boundingBox || defaultBoundingBox;
1100
+ }
1101
+ indexData.meta = { storybookLocation, repoRoot };
1102
+ console.log("[STORYBOOK_PLUGIN] Successfully enriched index.json", {
1103
+ entryCount: Object.keys(indexData.entries || {}).length,
1104
+ hasMetadata: !!indexData.meta
1105
+ });
1106
+ res.setHeader("Content-Type", "application/json");
1107
+ res.setHeader("Access-Control-Allow-Origin", "*");
1108
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1109
+ res.setHeader("Pragma", "no-cache");
1110
+ res.setHeader("Expires", "0");
1111
+ res.end(JSON.stringify(indexData));
1112
+ }).catch((error) => {
1113
+ console.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
1114
+ error: error instanceof Error ? error.message : String(error),
1115
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
1116
+ stack: error instanceof Error ? error.stack : void 0
1117
+ });
1118
+ res.statusCode = 500;
1119
+ res.setHeader("Content-Type", "application/json");
1120
+ res.end(
1121
+ JSON.stringify({ error: "Failed to fetch index", details: String(error) })
1122
+ );
1123
+ });
1124
+ return;
1125
+ }
1126
+ if (req.url?.startsWith("/api/capture-screenshot")) {
1127
+ const url = new URL(req.url, "http://localhost");
1128
+ const storyId = url.searchParams.get("storyId");
1129
+ const theme = url.searchParams.get("theme") || "light";
1130
+ const width = parseInt(url.searchParams.get("width") || "800", 10);
1131
+ const height = parseInt(url.searchParams.get("height") || "600", 10);
1132
+ if (!storyId) {
1133
+ res.statusCode = 400;
1134
+ res.setHeader("Content-Type", "application/json");
1135
+ res.end(JSON.stringify({ error: "storyId is required" }));
1136
+ return;
1137
+ }
1138
+ if (theme !== "light" && theme !== "dark") {
1139
+ res.statusCode = 400;
1140
+ res.setHeader("Content-Type", "application/json");
1141
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1142
+ return;
1143
+ }
1144
+ captureScreenshotBuffer(storyId, theme, width, height).then(({ buffer }) => {
1145
+ res.setHeader("Content-Type", "image/png");
1146
+ res.setHeader("Access-Control-Allow-Origin", "*");
1147
+ res.setHeader("Cache-Control", "no-cache");
1148
+ res.end(buffer);
1149
+ }).catch((error) => {
1150
+ console.error("Screenshot capture error:", error);
1151
+ res.statusCode = 500;
1152
+ res.setHeader("Content-Type", "application/json");
1153
+ res.end(
1154
+ JSON.stringify({
1155
+ error: "Failed to capture screenshot",
1156
+ details: String(error)
1157
+ })
1158
+ );
1159
+ });
1160
+ return;
1161
+ }
1162
+ if (req.url?.startsWith("/api/console-logs")) {
1163
+ const url = new URL(req.url, "http://localhost");
1164
+ const storyId = url.searchParams.get("storyId");
1165
+ const theme = url.searchParams.get("theme") || "light";
1166
+ if (!storyId) {
1167
+ res.statusCode = 400;
1168
+ res.setHeader("Content-Type", "application/json");
1169
+ res.end(JSON.stringify({ error: "storyId is required" }));
1170
+ return;
1171
+ }
1172
+ if (theme !== "light" && theme !== "dark") {
1173
+ res.statusCode = 400;
1174
+ res.setHeader("Content-Type", "application/json");
1175
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1176
+ return;
1177
+ }
1178
+ captureConsoleLogs(storyId, theme).then((result) => {
1179
+ res.setHeader("Content-Type", "application/json");
1180
+ res.setHeader("Access-Control-Allow-Origin", "*");
1181
+ res.setHeader("Cache-Control", "no-cache");
1182
+ res.end(JSON.stringify(result));
1183
+ }).catch((error) => {
1184
+ console.error("Console log capture error:", error);
1185
+ res.statusCode = 500;
1186
+ res.setHeader("Content-Type", "application/json");
1187
+ res.end(
1188
+ JSON.stringify({
1189
+ error: "Failed to capture console logs",
1190
+ details: String(error)
1191
+ })
1192
+ );
1193
+ });
1194
+ return;
1195
+ }
1196
+ if (req.url?.startsWith("/screenshots/")) {
1197
+ const screenshotPath = path7.join(
1198
+ process.cwd(),
1199
+ ".storybook-cache",
1200
+ req.url.replace("/screenshots/", "screenshots/")
1201
+ );
1202
+ const urlParts = req.url.replace("/screenshots/", "").split("/");
1203
+ const storyId = urlParts[0];
1204
+ const themeFile = urlParts[1];
1205
+ const theme = themeFile?.replace(".png", "");
1206
+ if (fs5.existsSync(screenshotPath)) {
1207
+ res.setHeader("Content-Type", "image/png");
1208
+ res.setHeader("Access-Control-Allow-Origin", "*");
1209
+ res.setHeader("Cache-Control", "public, max-age=3600");
1210
+ fs5.createReadStream(screenshotPath).pipe(res);
1211
+ return;
1212
+ }
1213
+ if (storyId && theme && (theme === "light" || theme === "dark")) {
1214
+ console.log(
1215
+ `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1216
+ );
1217
+ captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1218
+ const storyDir = path7.join(
1219
+ process.cwd(),
1220
+ ".storybook-cache",
1221
+ "screenshots",
1222
+ storyId
1223
+ );
1224
+ if (!fs5.existsSync(storyDir)) {
1225
+ fs5.mkdirSync(storyDir, { recursive: true });
1226
+ }
1227
+ fs5.writeFileSync(screenshotPath, buffer);
1228
+ res.setHeader("Content-Type", "image/png");
1229
+ res.setHeader("Access-Control-Allow-Origin", "*");
1230
+ res.setHeader("Cache-Control", "public, max-age=3600");
1231
+ res.end(buffer);
1232
+ }).catch((error) => {
1233
+ console.error(
1234
+ `[STORYBOOK_PLUGIN] Failed to generate screenshot: ${storyId}/${theme}`,
1235
+ error
1236
+ );
1237
+ res.statusCode = 500;
1238
+ res.setHeader("Content-Type", "application/json");
1239
+ res.setHeader("Access-Control-Allow-Origin", "*");
1240
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1241
+ res.end(
1242
+ JSON.stringify({
1243
+ error: "Failed to generate screenshot",
1244
+ details: String(error)
1245
+ })
1246
+ );
1247
+ });
1248
+ return;
1249
+ }
1250
+ res.statusCode = 404;
1251
+ res.setHeader("Access-Control-Allow-Origin", "*");
1252
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1253
+ res.end("Screenshot not found");
1254
+ return;
1255
+ }
1256
+ next();
1257
+ };
1258
+ var ONLOOK_PLUGIN_CLAIMS_GLOBAL = "__ONLOOK_STORYBOOK_PLUGIN_CLAIMS__";
1259
+ function claimRegistry() {
1260
+ const g = globalThis;
1261
+ let registry = g[ONLOOK_PLUGIN_CLAIMS_GLOBAL];
1262
+ if (!registry) {
1263
+ registry = /* @__PURE__ */ new WeakMap();
1264
+ g[ONLOOK_PLUGIN_CLAIMS_GLOBAL] = registry;
1265
+ }
1266
+ return registry;
1267
+ }
1268
+ function withDedupeApply(plugins, instanceId) {
1269
+ for (const plugin of plugins) {
1270
+ const prior = plugin.apply;
1271
+ plugin.apply = (config, env) => {
1272
+ const priorOk = prior === void 0 ? true : typeof prior === "string" ? env.command === prior : prior(config, env);
1273
+ if (!priorOk) return false;
1274
+ if (env === null || typeof env !== "object") return true;
1275
+ const registry = claimRegistry();
1276
+ const claimed = registry.get(env);
1277
+ if (claimed === void 0) {
1278
+ registry.set(env, instanceId);
1279
+ return true;
1280
+ }
1281
+ return claimed === instanceId;
1282
+ };
1283
+ }
1284
+ return plugins;
1285
+ }
1286
+ function storybookOnlookPlugin(options = {}) {
1287
+ if (process.env.CHROMATIC || process.env.CI) {
1288
+ console.log("[STORYBOOK_PLUGIN] Disabled in CI/Chromatic environment");
1289
+ return [];
1290
+ }
1291
+ const port = options.port ?? 6006;
1292
+ const allowedOrigins = [...DEFAULT_ALLOWED_ORIGINS, ...options.allowedOrigins ?? []];
1293
+ const hmrOverride = options.hmr;
1294
+ console.log("[STORYBOOK_PLUGIN] Plugin initialized", {
1295
+ port,
1296
+ allowedOrigins,
1297
+ storybookLocation,
1298
+ repoRoot,
1299
+ hmr: hmrOverride ?? null
1300
+ });
1301
+ const aliasEntries = options.resolveAliases === false ? [] : resolveTsconfigAliases(process.cwd());
1302
+ if (aliasEntries.length > 0) {
1303
+ console.log("[STORYBOOK_PLUGIN] Resolved tsconfig path aliases", {
1304
+ count: aliasEntries.length,
1305
+ aliases: aliasEntries.map((a) => a.find.source)
1306
+ });
1307
+ }
1308
+ const mainPlugin = {
1309
+ name: "storybook-onlook-plugin",
1310
+ config() {
1311
+ return {
1312
+ ...aliasEntries.length > 0 && {
1313
+ resolve: { alias: aliasEntries }
1314
+ },
1315
+ server: {
1316
+ // HMR override applies only when the caller explicitly opts in
1317
+ // (typically: Storybook fronted over HTTPS by a reverse proxy).
1318
+ // Locally, omitting this lets Vite use defaults
1319
+ // (ws://localhost:<port>) and HMR works as expected.
1320
+ ...hmrOverride && {
1321
+ hmr: {
1322
+ ...hmrOverride,
1323
+ port
1324
+ }
1325
+ },
1326
+ cors: {
1327
+ origin: allowedOrigins
1328
+ }
1329
+ }
1330
+ };
1331
+ },
1332
+ configureServer(server) {
1333
+ console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1334
+ server.middlewares.use(serveMetadataAndScreenshots);
1335
+ const cachedManifestPath = path7.join(
1336
+ process.cwd(),
1337
+ ".storybook-cache",
1338
+ "manifest.json"
1339
+ );
1340
+ refreshManifest(cachedManifestPath);
1341
+ let pending2;
1342
+ const scheduleRefresh = (file) => {
1343
+ if (file !== cachedManifestPath) return;
1344
+ if (pending2) clearTimeout(pending2);
1345
+ pending2 = setTimeout(() => {
1346
+ pending2 = void 0;
1347
+ refreshManifest(cachedManifestPath);
1348
+ }, 100);
1349
+ };
1350
+ server.watcher.on("add", scheduleRefresh);
1351
+ server.watcher.on("change", scheduleRefresh);
1352
+ const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1353
+ const resolvedStoryGlobs = storyGlobs.map(
1354
+ (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1355
+ );
1356
+ if (resolvedStoryGlobs.length > 0) {
1357
+ server.watcher.add(resolvedStoryGlobs);
1358
+ console.log("[STORYBOOK_PLUGIN] Extended watcher coverage", {
1359
+ storyGlobs: resolvedStoryGlobs
1360
+ });
1361
+ }
1362
+ const tailwindEntryPath = (() => {
1363
+ if (options.tailwindEntryCss === false) return null;
1364
+ if (typeof options.tailwindEntryCss === "string") {
1365
+ return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1366
+ }
1367
+ return detectTailwindEntryCss(process.cwd());
1368
+ })();
1369
+ if (tailwindEntryPath) {
1370
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan watcher enabled", {
1371
+ entry: tailwindEntryPath,
1372
+ autoDetected: options.tailwindEntryCss === void 0
1373
+ });
1374
+ const seenDirs = /* @__PURE__ */ new Set();
1375
+ let watcherReady = false;
1376
+ server.watcher.once("ready", () => {
1377
+ watcherReady = true;
1378
+ });
1379
+ server.watcher.on("add", (file) => {
1380
+ if (!/\.tsx?$/.test(file)) return;
1381
+ const dir = path7.dirname(file);
1382
+ const isNewDir = !seenDirs.has(dir);
1383
+ seenDirs.add(dir);
1384
+ if (!watcherReady || !isNewDir) return;
1385
+ try {
1386
+ const now = /* @__PURE__ */ new Date();
1387
+ fs5.utimesSync(tailwindEntryPath, now, now);
1388
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan for new dir", {
1389
+ dir,
1390
+ entry: tailwindEntryPath
1391
+ });
1392
+ } catch (err) {
1393
+ console.warn("[STORYBOOK_PLUGIN] Tailwind rescan: utimes failed", {
1394
+ entry: tailwindEntryPath,
1395
+ error: err instanceof Error ? err.message : String(err)
1396
+ });
1397
+ }
1398
+ });
1399
+ }
1400
+ server.httpServer?.once("listening", () => {
1401
+ const probePort = server.config.server.port ?? port;
1402
+ console.log("[STORYBOOK_PLUGIN] Server is listening", {
1403
+ port: probePort,
1404
+ host: server.config.server.host
1405
+ });
1406
+ void waitForIndexAndBroadcast(probePort);
1407
+ });
1408
+ },
1409
+ configurePreviewServer(server) {
1410
+ console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1411
+ server.middlewares.use(serveMetadataAndScreenshots);
1412
+ refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
1413
+ },
1414
+ handleHotUpdate: handleStoryFileChange
1415
+ };
1416
+ const envContainment = envContainmentPlugin(options.envContainment);
1417
+ const moduleLoadCatchAll = options.moduleLoadCatchAll === false ? null : moduleLoadCatchAllPlugin();
1418
+ return withDedupeApply(
1419
+ [
1420
+ ...envContainment ? [envContainment] : [],
1421
+ ...moduleLoadCatchAll ? [moduleLoadCatchAll] : [],
1422
+ componentLocPlugin(),
1423
+ mainPlugin,
1424
+ fastRefreshTolerantExportsPlugin(),
1425
+ softStoryRerenderPlugin()
1426
+ ],
1427
+ /* @__PURE__ */ Symbol("onlook-storybook-plugin-instance")
1428
+ );
1429
+ }
1430
+
1431
+ // src/preset/preset.ts
1432
+ var OPTION_KEY_MAP = {
1433
+ port: true,
1434
+ allowedOrigins: true,
1435
+ hmr: true,
1436
+ storyGlobs: true,
1437
+ tailwindEntryCss: true,
1438
+ envContainment: true,
1439
+ moduleLoadCatchAll: true,
1440
+ resolveAliases: true
1441
+ };
1442
+ var OPTION_KEYS = Object.keys(OPTION_KEY_MAP);
1443
+ function pluginOptionsFromPresetOptions(options) {
1444
+ const picked = {};
1445
+ if (options) {
1446
+ for (const key of OPTION_KEYS) {
1447
+ if (options[key] !== void 0) picked[key] = options[key];
1448
+ }
1449
+ }
1450
+ return picked;
1451
+ }
1452
+ async function viteFinal(config, options = {}) {
1453
+ const plugins = storybookOnlookPlugin(pluginOptionsFromPresetOptions(options));
1454
+ if (plugins.length === 0) return config;
1455
+ return { ...config, plugins: [...config.plugins ?? [], ...plugins] };
1456
+ }
1457
+
1458
+ export { pluginOptionsFromPresetOptions, viteFinal };