@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.
package/dist/index.js CHANGED
@@ -1,97 +1,309 @@
1
+ import { createRequire } from 'node:module';
2
+ import path7, { dirname, join, relative } from 'path';
3
+ import crypto, { createHash } from 'crypto';
1
4
  import fs5, { existsSync } from 'fs';
2
- import path, { dirname, join, relative } from 'path';
3
5
  import { fileURLToPath } from 'url';
4
- import autoStoryGenerator from '@takuma-ru/auto-story-generator';
5
- import { withDefaultConfig } from 'react-docgen-typescript';
6
6
  import generateModule from '@babel/generator';
7
7
  import { parse } from '@babel/parser';
8
8
  import traverseModule from '@babel/traverse';
9
9
  import * as t from '@babel/types';
10
- import crypto from 'crypto';
10
+ import { execFile } from 'child_process';
11
+ import { createRequire as createRequire$1 } from 'module';
12
+ import { promisify } from 'util';
11
13
  import { chromium } from 'playwright';
14
+ import { readCsf } from 'storybook/internal/csf-tools';
12
15
 
13
- // src/storybook-onlook-plugin.ts
14
- var FIXED_MARKER = "// @onlook-fixed";
15
- var parser = withDefaultConfig({
16
- shouldExtractLiteralValuesFromEnum: true,
17
- shouldRemoveUndefinedFromOptional: true,
18
- propFilter: (prop) => {
19
- if (prop.parent?.fileName.includes("node_modules")) return false;
20
- return true;
21
- }
22
- });
23
- function resolveComponentPath(storyFilePath) {
24
- const dir = path.dirname(storyFilePath);
25
- const parentDir = path.dirname(dir);
26
- const storyName = path.basename(storyFilePath);
27
- const componentName = storyName.replace(".stories.tsx", ".tsx");
28
- const componentPath = path.join(parentDir, componentName);
29
- return fs5.existsSync(componentPath) ? componentPath : null;
30
- }
31
- function generateArgTypes(componentPath) {
32
- try {
33
- const docs = parser.parse(componentPath);
34
- if (docs.length === 0) return null;
35
- const doc = docs[0];
36
- if (!doc) return null;
37
- const argTypes = {};
38
- for (const [name, prop] of Object.entries(doc.props)) {
39
- if (prop.type.name.includes("=>") || prop.type.name === "Function") {
40
- continue;
41
- }
42
- const argType = {};
43
- if (prop.description) {
44
- argType.description = prop.description;
16
+ globalThis.require = createRequire(import.meta.url);
17
+
18
+ // src/containment-contract/containment-contract.ts
19
+ var ONLOOK_CONTAINMENT_CONTRACT_VERSION = 1;
20
+ var ONLOOK_CONTAINED_MODULES_GLOBAL = "__ONLOOK_CONTAINED_MODULES__";
21
+ var ONLOOK_CONTAINED_PROP = "__onlookContained";
22
+ var ONLOOK_CONTAINMENT_CONTRACT_GLOBAL = "__ONLOOK_CONTAINMENT_CONTRACT__";
23
+ var ONLOOK_CONTAINED_EVENT = "onlook/contained";
24
+ var ONLOOK_RENDERED_EVENT = "onlook/rendered";
25
+ var ONLOOK_UNCONTAINED_ERROR_EVENT = "onlook/uncontained-error";
26
+ var ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
27
+ var ONLOOK_RENDER_STORY_ATTR = "data-onlook-story-id";
28
+ var ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
29
+ var ENV_CONTAINMENT_PLUGIN_NAME = "onlook-env-containment";
30
+ var ENV_CONTAINMENT_VIRTUAL_PREFIX = "\0onlook-env-containment:";
31
+ var VALID_IDENT_RE = /^[A-Za-z_$][\w$]*$/;
32
+ var MATCH_SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
33
+ function normalizeSlashes(p) {
34
+ return p.replace(/\\/g, "/");
35
+ }
36
+ function stripQuery(id) {
37
+ const q = id.indexOf("?");
38
+ return q === -1 ? id : id.slice(0, q);
39
+ }
40
+ function envContainmentPlugin(options) {
41
+ const substitutions = (options?.substitute ?? []).filter(
42
+ (s) => typeof s.module === "string" && s.module.length > 0
43
+ );
44
+ const defineEntries = options?.define ?? {};
45
+ if (substitutions.length === 0 && Object.keys(defineEntries).length === 0) {
46
+ return null;
47
+ }
48
+ const byModule = /* @__PURE__ */ new Map();
49
+ for (const sub of substitutions) {
50
+ byModule.set(normalizeSlashes(sub.module).replace(/^\.\//, ""), sub);
51
+ }
52
+ const targetBasenames = /* @__PURE__ */ new Set();
53
+ for (const rel of byModule.keys()) {
54
+ const base = rel.slice(rel.lastIndexOf("/") + 1);
55
+ targetBasenames.add(base.replace(/\.[^.]+$/, ""));
56
+ }
57
+ const specifierMightMatch = (cleaned) => {
58
+ const segment = cleaned.slice(cleaned.lastIndexOf("/") + 1);
59
+ return targetBasenames.has(segment.replace(/\.[^.]+$/, ""));
60
+ };
61
+ const resolveOutcomes = /* @__PURE__ */ new Map();
62
+ const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
63
+ const matchTarget = (absPath) => {
64
+ const norm = normalizeSlashes(path7.normalize(absPath));
65
+ for (const rel of byModule.keys()) {
66
+ for (const root of roots) {
67
+ const target = `${root}/${rel}`;
68
+ for (const suffix of MATCH_SUFFIXES) {
69
+ if (`${norm}${suffix}` === target) return rel;
70
+ }
45
71
  }
46
- if (prop.type.name === "enum" && prop.type.value) {
47
- argType.control = { type: "select" };
48
- argType.options = prop.type.value.map(
49
- (v) => v.value
50
- );
72
+ }
73
+ return null;
74
+ };
75
+ return {
76
+ name: ENV_CONTAINMENT_PLUGIN_NAME,
77
+ // `pre` so we see specifiers before Vite's alias/resolve pipeline and
78
+ // before other resolveId hooks (e.g. tsconfig-paths) claim them.
79
+ enforce: "pre",
80
+ config() {
81
+ const keys = Object.entries(defineEntries);
82
+ if (keys.length === 0) return {};
83
+ const define = {};
84
+ for (const [key, value] of keys) {
85
+ const serialized = JSON.stringify(value);
86
+ define[`process.env.${key}`] = serialized;
87
+ define[`import.meta.env.${key}`] = serialized;
51
88
  }
52
- if (prop.type.name === "boolean") {
53
- argType.control = { type: "boolean" };
89
+ return { define };
90
+ },
91
+ configResolved(config) {
92
+ if (config.root) roots.add(normalizeSlashes(config.root));
93
+ },
94
+ async resolveId(source, importer) {
95
+ if (byModule.size === 0) return null;
96
+ if (source.startsWith("\0")) return null;
97
+ const cleaned = stripQuery(source);
98
+ let direct = null;
99
+ if (path7.isAbsolute(cleaned)) {
100
+ direct = cleaned;
101
+ } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
102
+ direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
54
103
  }
55
- if (prop.type.name === "number") {
56
- argType.control = { type: "number" };
104
+ if (direct) {
105
+ const rel = matchTarget(direct);
106
+ if (rel) return ENV_CONTAINMENT_VIRTUAL_PREFIX + rel;
107
+ return null;
57
108
  }
58
- if (Object.keys(argType).length > 0) {
59
- argTypes[name] = argType;
109
+ if (!specifierMightMatch(cleaned)) return null;
110
+ if (typeof this.resolve !== "function") return null;
111
+ const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
112
+ const outcomeKey = `${importerDir}|${source}`;
113
+ const memoized = resolveOutcomes.get(outcomeKey);
114
+ if (memoized !== void 0) return memoized;
115
+ const resolved = await this.resolve(source, importer, { skipSelf: true });
116
+ let outcome = null;
117
+ if (resolved?.id && !resolved.external) {
118
+ const rel = matchTarget(stripQuery(resolved.id));
119
+ if (rel) outcome = ENV_CONTAINMENT_VIRTUAL_PREFIX + rel;
60
120
  }
121
+ resolveOutcomes.set(outcomeKey, outcome);
122
+ return outcome;
123
+ },
124
+ load(id) {
125
+ if (!id.startsWith(ENV_CONTAINMENT_VIRTUAL_PREFIX)) return null;
126
+ const rel = id.slice(ENV_CONTAINMENT_VIRTUAL_PREFIX.length);
127
+ const sub = byModule.get(rel);
128
+ if (!sub) return null;
129
+ return buildStandInSource(sub);
61
130
  }
62
- return Object.keys(argTypes).length > 0 ? argTypes : null;
63
- } catch (err) {
64
- console.error(`[AutoStories] Failed to parse component: ${componentPath}`, err);
65
- return null;
66
- }
131
+ };
67
132
  }
68
- function enrichStoryFile(storyFilePath) {
133
+ function buildStandInSource(sub) {
134
+ const marker = {
135
+ modulePath: sub.module,
136
+ reason: "env-validation",
137
+ envKeys: sub.envKeys ?? [],
138
+ ...sub.schemaKind ? { schemaKind: sub.schemaKind } : {}
139
+ };
140
+ const named = Array.from(
141
+ new Set(
142
+ (sub.exports ?? []).filter(
143
+ (name) => VALID_IDENT_RE.test(name) && name !== "default"
144
+ )
145
+ )
146
+ );
147
+ return [
148
+ "// Generated by @onlook/storybook-plugin (env containment).",
149
+ `// The real module (${sub.module}) validates environment variables at`,
150
+ "// module scope and throws on import when keys are missing. This stand-in",
151
+ "// defers the throw to property access so Storybook can boot and the",
152
+ "// Onlook error boundary can attribute the failure to the module.",
153
+ `const __onlookMarker = ${JSON.stringify(marker)};`,
154
+ "const __onlookGlobal = globalThis;",
155
+ `__onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL} =`,
156
+ ` __onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL} || {};`,
157
+ `__onlookGlobal.${ONLOOK_CONTAINED_MODULES_GLOBAL}[__onlookMarker.modulePath] = __onlookMarker;`,
158
+ "function __onlookContainedError(access) {",
159
+ " const keys = __onlookMarker.envKeys.length",
160
+ " ? ' Missing env keys: ' + __onlookMarker.envKeys.join(', ') + '.'",
161
+ " : '';",
162
+ " const err = new Error(",
163
+ " '[onlook] ' + __onlookMarker.modulePath +",
164
+ " ' was contained: it validates environment variables at module scope' +",
165
+ " ' and the required keys are not set. Accessed: ' + access + '.' + keys,",
166
+ " );",
167
+ ` err.${ONLOOK_CONTAINED_PROP} = __onlookMarker;`,
168
+ " return err;",
169
+ "}",
170
+ "function __onlookStandIn(exportName) {",
171
+ " return new Proxy(function () {}, {",
172
+ " get(_target, prop) {",
173
+ ` if (prop === '${ONLOOK_CONTAINED_PROP}') return __onlookMarker;`,
174
+ " if (typeof prop === 'symbol' || prop === 'then') return undefined;",
175
+ " throw __onlookContainedError(exportName + '.' + String(prop));",
176
+ " },",
177
+ " apply() {",
178
+ " throw __onlookContainedError(exportName + '()');",
179
+ " },",
180
+ " construct() {",
181
+ " throw __onlookContainedError('new ' + exportName + '()');",
182
+ " },",
183
+ " });",
184
+ "}",
185
+ "export default __onlookStandIn('default');",
186
+ ...named.map(
187
+ (name) => `export const ${name} = __onlookStandIn(${JSON.stringify(name)});`
188
+ ),
189
+ ""
190
+ ].join("\n");
191
+ }
192
+
193
+ // src/module-load-catch-all/module-load-catch-all.ts
194
+ var STORYBOOK_VIRTUAL_STORIES_ID = "virtual:/@storybook/builder-vite/storybook-stories.js";
195
+ var RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID = `\0${STORYBOOK_VIRTUAL_STORIES_ID}`;
196
+ var MODULE_LOAD_CATCH_ALL_PLUGIN_NAME = "onlook-module-load-catch-all";
197
+ var IMPORT_FN_CALL = "return await importers[path]();";
198
+ var INJECTED_BINDING = "__onlookSyntheticCsfModule";
199
+ async function onlookSyntheticCsfModule(importPath, loadError, containedProp) {
200
+ const original = loadError instanceof Error ? loadError : new Error(String(loadError));
201
+ const synthetic = new Error(
202
+ `Story module failed to load: ${importPath}
203
+ ${original.message}`
204
+ );
205
+ const marker = original[containedProp];
206
+ if (marker !== void 0) {
207
+ synthetic[containedProp] = marker;
208
+ }
209
+ synthetic.cause = original;
69
210
  try {
70
- const content = fs5.readFileSync(storyFilePath, "utf-8");
71
- if (content.includes(FIXED_MARKER)) {
72
- return;
73
- }
74
- if (content.includes("argTypes:")) {
75
- return;
76
- }
77
- const componentPath = resolveComponentPath(storyFilePath);
78
- if (!componentPath) return;
79
- const argTypes = generateArgTypes(componentPath);
80
- if (!argTypes) return;
81
- const argTypesStr = JSON.stringify(argTypes, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
82
- const enriched = content.replace(
83
- /};\s*\nexport default meta;/,
84
- ` argTypes: ${argTypesStr},
85
- };
86
- export default meta;`
211
+ console.error(
212
+ "[ONLOOK] Story module failed to load \u2014 serving contained synthetic stories.",
213
+ { importPath, error: original }
87
214
  );
88
- if (enriched !== content) {
89
- fs5.writeFileSync(storyFilePath, enriched);
90
- console.log(`[AutoStories] Enriched ${path.basename(storyFilePath)} with argTypes`);
215
+ } catch {
216
+ }
217
+ const stories = [];
218
+ try {
219
+ const response = await fetch("./index.json");
220
+ if (response?.ok) {
221
+ const index = await response.json();
222
+ for (const entry of Object.values(index.entries ?? {})) {
223
+ if (!entry || entry.type !== "story") continue;
224
+ if (entry.importPath !== importPath) continue;
225
+ if (typeof entry.id !== "string") continue;
226
+ stories.push({
227
+ id: entry.id,
228
+ name: typeof entry.name === "string" ? entry.name : entry.id
229
+ });
230
+ }
91
231
  }
92
- } catch (err) {
93
- console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
232
+ } catch {
233
+ }
234
+ if (stories.length === 0) {
235
+ try {
236
+ const search = typeof window !== "undefined" && window.location ? window.location.search : "";
237
+ const urlId = new URLSearchParams(search).get("id");
238
+ if (urlId) stories.push({ id: urlId, name: "Needs setup" });
239
+ } catch {
240
+ }
241
+ }
242
+ const moduleExports = { default: {} };
243
+ const render = () => {
244
+ throw synthetic;
245
+ };
246
+ if (stories.length === 0) {
247
+ moduleExports.OnlookContained = { name: "Needs setup", render };
248
+ return moduleExports;
94
249
  }
250
+ let i = 0;
251
+ for (const story of stories) {
252
+ moduleExports[`OnlookContained${i}`] = {
253
+ name: story.name,
254
+ // Forces the exact index story id — export names can't be recovered
255
+ // from a module that never evaluated.
256
+ parameters: { __id: story.id },
257
+ render
258
+ };
259
+ i += 1;
260
+ }
261
+ return moduleExports;
262
+ }
263
+ function wrapImportFn(code) {
264
+ if (code.includes(INJECTED_BINDING)) return null;
265
+ if (!code.includes(IMPORT_FN_CALL)) return null;
266
+ const wrappedCall = [
267
+ "try {",
268
+ " return await importers[path]();",
269
+ " } catch (__onlookLoadError) {",
270
+ ` return await ${INJECTED_BINDING}(path, __onlookLoadError, ${JSON.stringify(ONLOOK_CONTAINED_PROP)});`,
271
+ " }"
272
+ ].join("\n");
273
+ const body = code.replace(IMPORT_FN_CALL, wrappedCall);
274
+ return [
275
+ body,
276
+ "",
277
+ "// Injected by @onlook/storybook-plugin (module-load catch-all, U7).",
278
+ `const ${INJECTED_BINDING} = ${onlookSyntheticCsfModule.toString()};`,
279
+ ""
280
+ ].join("\n");
281
+ }
282
+ function moduleLoadCatchAllPlugin() {
283
+ let warnedDrift = false;
284
+ return {
285
+ name: MODULE_LOAD_CATCH_ALL_PLUGIN_NAME,
286
+ // 'pre' so the wrap sees builder-vite's raw codegen (served from its
287
+ // `load` hook) before normal-phase transforms (e.g. plugin-react, whose
288
+ // default filter matches the virtual id's `.js` suffix) reshape it.
289
+ enforce: "pre",
290
+ transform(code, id) {
291
+ if (id !== RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID && id !== STORYBOOK_VIRTUAL_STORIES_ID) {
292
+ return null;
293
+ }
294
+ const wrapped = wrapImportFn(code);
295
+ if (wrapped === null) {
296
+ if (!warnedDrift && !code.includes(INJECTED_BINDING)) {
297
+ warnedDrift = true;
298
+ console.warn(
299
+ "[STORYBOOK_PLUGIN] module-load-catch-all: unrecognized importFn codegen \u2014 passing through (module-load failures will surface as raw Storybook overlays)."
300
+ );
301
+ }
302
+ return null;
303
+ }
304
+ return { code: wrapped, map: null };
305
+ }
306
+ };
95
307
  }
96
308
  function componentLocPlugin(options = {}) {
97
309
  const include = options.include ?? /\.(jsx|tsx)$/;
@@ -109,13 +321,14 @@ function componentLocPlugin(options = {}) {
109
321
  const filepath = id.split("?", 1)[0];
110
322
  if (!filepath || filepath.includes("node_modules")) return null;
111
323
  if (!include.test(filepath)) return null;
324
+ if (/\.stories\.(jsx?|tsx?)$/.test(filepath)) return null;
112
325
  const ast = parse(code, {
113
326
  sourceType: "module",
114
327
  plugins: ["jsx", "typescript"],
115
328
  sourceFilename: filepath
116
329
  });
117
330
  let mutated = false;
118
- const relativePath = path.relative(root, filepath);
331
+ const relativePath = path7.relative(root, filepath);
119
332
  traverse(ast, {
120
333
  JSXElement(nodePath) {
121
334
  const opening = nodePath.node.openingElement;
@@ -152,9 +365,31 @@ function componentLocPlugin(options = {}) {
152
365
  }
153
366
  };
154
367
  }
155
- var CACHE_DIR = path.join(process.cwd(), ".storybook-cache");
156
- var SCREENSHOTS_DIR = path.join(CACHE_DIR, "screenshots");
157
- var MANIFEST_PATH = path.join(CACHE_DIR, "manifest.json");
368
+
369
+ // src/fast-refresh-tolerant-exports/fast-refresh-tolerant-exports.ts
370
+ function fastRefreshTolerantExportsPlugin() {
371
+ const REGISTER_CALL_RE = /(\w+)\.registerExportsForReactRefresh\((["'])([^"'\n]+)\2,\s*([A-Za-z_$][\w$]*)\)/;
372
+ return {
373
+ name: "onbook-fast-refresh-tolerant-exports",
374
+ enforce: "post",
375
+ apply: "serve",
376
+ transform(code, id) {
377
+ if (!/\.(tsx|jsx)$/.test(id)) return null;
378
+ if (id.includes("node_modules")) return null;
379
+ const m = code.match(REGISTER_CALL_RE);
380
+ if (!m) return null;
381
+ const [fullMatch, , , idValue, exportsVar] = m;
382
+ if (!idValue || !exportsVar) return null;
383
+ const idLiteral = JSON.stringify(idValue);
384
+ 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]||[];}})();";
385
+ const out = code.replace(fullMatch, fullMatch + postRegister);
386
+ return out === code ? null : { code: out, map: null };
387
+ }
388
+ };
389
+ }
390
+ var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
391
+ var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
392
+ var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
158
393
  var VIEWPORT_WIDTH = 1920;
159
394
  var VIEWPORT_HEIGHT = 1080;
160
395
  var MIN_COMPONENT_WIDTH = 420;
@@ -201,22 +436,86 @@ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
201
436
  };
202
437
  saveManifest(manifest);
203
438
  }
204
- var browser = null;
439
+ var execFileAsync = promisify(execFile);
440
+ var require2 = createRequire$1(import.meta.url);
441
+ var browserPromise = null;
442
+ var installPromise = null;
443
+ var isMissingExecutableError = (err) => {
444
+ const msg = err instanceof Error ? err.message : String(err);
445
+ 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.
446
+ /chromium-\d+\/chrome-linux\/chrome/i.test(msg);
447
+ };
448
+ function resolvePlaywrightCli() {
449
+ const pkgJson = require2.resolve("playwright-core/package.json");
450
+ return join(dirname(pkgJson), "cli.js");
451
+ }
452
+ async function installChromium() {
453
+ if (!installPromise) {
454
+ installPromise = (async () => {
455
+ const cliPath = resolvePlaywrightCli();
456
+ console.log(
457
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
458
+ );
459
+ try {
460
+ const { stdout, stderr } = await execFileAsync(
461
+ process.execPath,
462
+ [cliPath, "install", "--force", "chromium"],
463
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
464
+ );
465
+ if (stdout)
466
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
467
+ if (stderr)
468
+ console.log(
469
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
470
+ );
471
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
472
+ } catch (err) {
473
+ const detail = err instanceof Error ? err.message : String(err);
474
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
475
+ throw new Error(`Playwright chromium install failed: ${detail}`);
476
+ } finally {
477
+ installPromise = null;
478
+ }
479
+ })();
480
+ }
481
+ return installPromise;
482
+ }
483
+ async function launchWithSelfHeal() {
484
+ try {
485
+ return await chromium.launch({ headless: true });
486
+ } catch (err) {
487
+ if (!isMissingExecutableError(err)) throw err;
488
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
489
+ await installChromium();
490
+ try {
491
+ return await chromium.launch({ headless: true });
492
+ } catch (postInstallErr) {
493
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
494
+ console.error(
495
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
496
+ );
497
+ throw new Error(
498
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
499
+ );
500
+ }
501
+ }
502
+ }
205
503
  async function getBrowser() {
206
- if (!browser) {
207
- browser = await chromium.launch({
208
- headless: true
504
+ if (!browserPromise) {
505
+ browserPromise = launchWithSelfHeal().catch((err) => {
506
+ browserPromise = null;
507
+ throw err;
209
508
  });
210
509
  }
211
- return browser;
510
+ return browserPromise;
212
511
  }
213
512
  function getScreenshotPath(storyId, theme) {
214
- const storyDir = path.join(SCREENSHOTS_DIR, storyId);
215
- return path.join(storyDir, `${theme}.png`);
513
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
514
+ return path7.join(storyDir, `${theme}.png`);
216
515
  }
217
516
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
218
- const browser2 = await getBrowser();
219
- const context = await browser2.newContext({
517
+ const browser = await getBrowser();
518
+ const context = await browser.newContext({
220
519
  viewport: { width, height },
221
520
  deviceScaleFactor: 2
222
521
  });
@@ -299,7 +598,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
299
598
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
300
599
  try {
301
600
  ensureCacheDirectories();
302
- const storyDir = path.join(SCREENSHOTS_DIR, storyId);
601
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
303
602
  if (!fs5.existsSync(storyDir)) {
304
603
  fs5.mkdirSync(storyDir, { recursive: true });
305
604
  }
@@ -320,12 +619,93 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
320
619
  }
321
620
  }
322
621
 
622
+ // src/screenshot-service/screenshot-service.ts
623
+ process.env.ONBOOK_PROXY_TOKEN ?? null;
624
+ process.env.ONBOOK_BROADCAST_URL ?? null;
625
+
626
+ // src/utils/notifyOnbookIndexChanged/notifyOnbookIndexChanged.ts
627
+ var DEBOUNCE_MS = 300;
628
+ var REQUEST_TIMEOUT_MS = 5e3;
629
+ var pending = null;
630
+ var inFlight = false;
631
+ var pendingTrailingCall = false;
632
+ function notifyOnbookIndexChanged() {
633
+ const proxyToken = process.env.ONBOOK_PROXY_TOKEN;
634
+ const broadcastUrl = process.env.ONBOOK_BROADCAST_URL;
635
+ if (!proxyToken || !broadcastUrl) return;
636
+ if (pending) clearTimeout(pending);
637
+ pending = setTimeout(() => {
638
+ pending = null;
639
+ if (inFlight) {
640
+ pendingTrailingCall = true;
641
+ return;
642
+ }
643
+ void fire(proxyToken, broadcastUrl);
644
+ }, DEBOUNCE_MS);
645
+ }
646
+ async function fire(proxyToken, broadcastUrl) {
647
+ inFlight = true;
648
+ try {
649
+ const res = await fetch(broadcastUrl, {
650
+ method: "POST",
651
+ headers: {
652
+ Authorization: `Bearer ${proxyToken}`,
653
+ "Content-Type": "application/json"
654
+ },
655
+ body: JSON.stringify({ event: { type: "STORYBOOK_INDEX_CHANGED" } }),
656
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
657
+ });
658
+ if (!res.ok) {
659
+ console.warn("[STORYBOOK_PLUGIN] broadcast non-2xx", {
660
+ status: res.status,
661
+ statusText: res.statusText
662
+ });
663
+ }
664
+ } catch (err) {
665
+ console.warn(
666
+ "[STORYBOOK_PLUGIN] index-changed broadcast failed",
667
+ err instanceof Error ? err.message : String(err)
668
+ );
669
+ } finally {
670
+ inFlight = false;
671
+ if (pendingTrailingCall) {
672
+ pendingTrailingCall = false;
673
+ notifyOnbookIndexChanged();
674
+ }
675
+ }
676
+ }
677
+
678
+ // src/utils/notifyOnbookIndexChanged/waitForIndexAndBroadcast.ts
679
+ var PROBE_INTERVAL_MS = 500;
680
+ var PROBE_CEILING_MS = 5 * 60 * 1e3;
681
+ async function waitForIndexAndBroadcast(port = 6006) {
682
+ const url = `http://localhost:${port}/onbook-index.json`;
683
+ const deadline = Date.now() + PROBE_CEILING_MS;
684
+ while (Date.now() < deadline) {
685
+ try {
686
+ const res = await fetch(url, {
687
+ signal: AbortSignal.timeout(2e3)
688
+ });
689
+ if (res.ok) {
690
+ notifyOnbookIndexChanged();
691
+ return;
692
+ }
693
+ } catch {
694
+ }
695
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
696
+ }
697
+ console.warn("[STORYBOOK_PLUGIN] waitForIndexAndBroadcast hit ceiling without a 2xx", {
698
+ url,
699
+ ceilingMs: PROBE_CEILING_MS
700
+ });
701
+ }
702
+
323
703
  // src/handlers/handleStoryFileChange/handleStoryFileChange.ts
324
704
  var cachedIndex = null;
325
705
  var indexFetchPromise = null;
326
706
  var pendingFiles = /* @__PURE__ */ new Set();
327
707
  var debounceTimer = null;
328
- var DEBOUNCE_MS = 500;
708
+ var DEBOUNCE_MS2 = 500;
329
709
  async function fetchStorybookIndex() {
330
710
  try {
331
711
  const response = await fetch("http://localhost:6006/index.json");
@@ -339,7 +719,7 @@ async function fetchStorybookIndex() {
339
719
  }
340
720
  function getStoriesForFile(filePath) {
341
721
  if (!cachedIndex) return [];
342
- const fileName = path.basename(filePath);
722
+ const fileName = path7.basename(filePath);
343
723
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
344
724
  }
345
725
  async function regenerateScreenshotsForFiles(files) {
@@ -384,6 +764,7 @@ async function regenerateScreenshotsForFiles(files) {
384
764
  function handleStoryFileChange({ file, modules }) {
385
765
  if (file.endsWith(".stories.tsx") || file.endsWith(".stories.ts")) {
386
766
  console.log(`[Screenshots] Story file changed: ${file}`);
767
+ notifyOnbookIndexChanged();
387
768
  if (!cachedIndex && !indexFetchPromise) {
388
769
  indexFetchPromise = fetchStorybookIndex();
389
770
  }
@@ -400,10 +781,225 @@ function handleStoryFileChange({ file, modules }) {
400
781
  } catch (error) {
401
782
  console.error("[Screenshots] Error regenerating screenshots:", error);
402
783
  }
403
- }, DEBOUNCE_MS);
784
+ }, DEBOUNCE_MS2);
404
785
  return modules;
405
786
  }
406
787
  }
788
+
789
+ // src/screenshot-service/utils/console-logs/console-logs.ts
790
+ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
791
+ const browser = await getBrowser();
792
+ const context = await browser.newContext({
793
+ viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }
794
+ });
795
+ const page = await context.newPage();
796
+ const logs = [];
797
+ const pageErrors = [];
798
+ page.on("console", (msg) => {
799
+ const level = msg.type();
800
+ logs.push({
801
+ level: level === "warning" ? "warn" : level,
802
+ text: msg.text(),
803
+ timestamp: Date.now()
804
+ });
805
+ });
806
+ page.on("pageerror", (err) => {
807
+ pageErrors.push(err.message);
808
+ });
809
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
810
+ try {
811
+ await page.goto(url, { timeout: timeoutMs });
812
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
813
+ await page.waitForLoadState("load", { timeout: timeoutMs });
814
+ try {
815
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
816
+ } catch {
817
+ }
818
+ await new Promise((r) => setTimeout(r, 1e3));
819
+ return { logs, pageErrors, url };
820
+ } finally {
821
+ await context.close();
822
+ }
823
+ }
824
+
825
+ // src/soft-story-rerender/soft-story-rerender.ts
826
+ function softStoryRerenderPlugin() {
827
+ let warnedOnce = false;
828
+ return {
829
+ name: "onbook-soft-story-rerender",
830
+ enforce: "post",
831
+ apply: "serve",
832
+ transform(code, _id) {
833
+ if (!code.includes("__STORYBOOK_PREVIEW__.onStoriesChanged")) return null;
834
+ 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*\}\);/;
835
+ if (!pattern.test(code)) {
836
+ if (!warnedOnce) {
837
+ warnedOnce = true;
838
+ console.warn(
839
+ "[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."
840
+ );
841
+ }
842
+ return null;
843
+ }
844
+ const replacement = `import.meta.hot.accept($QUOTE$$PATH$$QUOTE$, async (newModule) => {
845
+ // ONL-1176 soft rerender \u2014 call rerender() on each active StoryRender
846
+ // instead of preview.onStoriesChanged() (which tears down + remounts).
847
+ // This accept handler fires in EVERY canvas iframe whenever ANY story
848
+ // file changes (every iframe's preview bundle imports the same virtual
849
+ // stories index). We identity-check each render's unboundStoryFn so
850
+ // iframes whose underlying story file didn't change stay perfectly
851
+ // still \u2014 no 1-frame no-op tick.
852
+ const preview = window.__STORYBOOK_PREVIEW__;
853
+ if (!preview || !preview.storyStoreValue) {
854
+ window.__STORYBOOK_PREVIEW__?.onStoriesChanged({ importFn: newModule.importFn });
855
+ return;
856
+ }
857
+ try {
858
+ await preview.storyStoreValue.onStoriesChanged({ importFn: newModule.importFn });
859
+ const renders = preview.storyRenders || [];
860
+ if (renders.length === 0) {
861
+ preview.onStoriesChanged({ importFn: newModule.importFn });
862
+ return;
863
+ }
864
+ await Promise.all(
865
+ renders.map(async (r) => {
866
+ try {
867
+ const nextStory = await preview.storyStoreValue.loadStory({ storyId: r.id });
868
+ if (!nextStory) return;
869
+ const prev = r.story;
870
+ const unchanged =
871
+ prev &&
872
+ prev.unboundStoryFn === nextStory.unboundStoryFn &&
873
+ prev.storyFn === nextStory.storyFn &&
874
+ prev.moduleExport === nextStory.moduleExport &&
875
+ prev.playFunction === nextStory.playFunction;
876
+ if (unchanged) return;
877
+ r.story = nextStory;
878
+ // Stories using the \`mount\` API can't be rerendered in place
879
+ // (their play function owns mounting), so fall back to remount.
880
+ if (nextStory.usesMount && r.remount) {
881
+ return r.remount();
882
+ }
883
+ return r.rerender();
884
+ } catch (innerErr) {
885
+ console.warn('[onbook] rerender failed for story', r.id, innerErr);
886
+ }
887
+ }),
888
+ );
889
+ } catch (err) {
890
+ console.warn('[onbook] soft rerender failed, falling back', err);
891
+ preview.onStoriesChanged({ importFn: newModule.importFn });
892
+ }
893
+ });`;
894
+ const out = code.replace(pattern, (_match, quote, path8) => {
895
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
896
+ });
897
+ return out === code ? null : { code: out, map: null };
898
+ }
899
+ };
900
+ }
901
+ var TSCONFIG_CANDIDATES = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json"];
902
+ function resolveTsconfigAliases(root) {
903
+ try {
904
+ const config = readTsconfigAliasConfig(root);
905
+ if (!config) return [];
906
+ const base = path7.resolve(root, config.baseUrl ?? ".");
907
+ const aliases = [];
908
+ for (const [key, targets] of Object.entries(config.paths)) {
909
+ const target = targets[0];
910
+ if (!target) continue;
911
+ if (key.endsWith("/*") && target.endsWith("/*")) {
912
+ const findPrefix = key.slice(0, -1);
913
+ const targetDir = path7.resolve(base, target.slice(0, -2));
914
+ aliases.push({
915
+ find: new RegExp(`^${escapeRegExp(findPrefix)}`),
916
+ replacement: `${targetDir}/`
917
+ });
918
+ } else if (!key.includes("*") && !target.includes("*")) {
919
+ aliases.push({
920
+ find: new RegExp(`^${escapeRegExp(key)}$`),
921
+ replacement: path7.resolve(base, target)
922
+ });
923
+ }
924
+ }
925
+ return aliases.sort((a, b) => b.find.source.length - a.find.source.length);
926
+ } catch {
927
+ return [];
928
+ }
929
+ }
930
+ function readTsconfigAliasConfig(root) {
931
+ for (const candidate of TSCONFIG_CANDIDATES) {
932
+ const abs = path7.join(root, candidate);
933
+ let text;
934
+ try {
935
+ text = fs5.readFileSync(abs, "utf-8");
936
+ } catch {
937
+ continue;
938
+ }
939
+ let parsed;
940
+ try {
941
+ parsed = JSON.parse(stripJsonComments(text));
942
+ } catch {
943
+ continue;
944
+ }
945
+ const paths = parsed.compilerOptions?.paths;
946
+ if (paths && typeof paths === "object" && Object.keys(paths).length > 0) {
947
+ const baseUrl = parsed.compilerOptions?.baseUrl;
948
+ return typeof baseUrl === "string" ? { baseUrl, paths } : { paths };
949
+ }
950
+ }
951
+ return null;
952
+ }
953
+ function escapeRegExp(value) {
954
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
955
+ }
956
+ function stripJsonComments(text) {
957
+ let out = "";
958
+ let inString = false;
959
+ let inLine = false;
960
+ let inBlock = false;
961
+ for (let i = 0; i < text.length; i++) {
962
+ const c = text.charAt(i);
963
+ const next = text.charAt(i + 1);
964
+ if (inLine) {
965
+ if (c === "\n") {
966
+ inLine = false;
967
+ out += c;
968
+ }
969
+ continue;
970
+ }
971
+ if (inBlock) {
972
+ if (c === "*" && next === "/") {
973
+ inBlock = false;
974
+ i++;
975
+ }
976
+ continue;
977
+ }
978
+ if (inString) {
979
+ out += c;
980
+ if (c === "\\") {
981
+ out += next;
982
+ i++;
983
+ } else if (c === '"') {
984
+ inString = false;
985
+ }
986
+ continue;
987
+ }
988
+ if (c === '"') {
989
+ inString = true;
990
+ out += c;
991
+ } else if (c === "/" && next === "/") {
992
+ inLine = true;
993
+ i++;
994
+ } else if (c === "/" && next === "*") {
995
+ inBlock = true;
996
+ i++;
997
+ } else {
998
+ out += c;
999
+ }
1000
+ }
1001
+ return out;
1002
+ }
407
1003
  function findGitRoot(startPath) {
408
1004
  let currentPath = startPath;
409
1005
  while (currentPath !== dirname(currentPath)) {
@@ -422,86 +1018,71 @@ var gitRoot = findGitRoot(storybookDir);
422
1018
  var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
423
1019
  var repoRoot = gitRoot || process.cwd();
424
1020
  var DEFAULT_ALLOWED_ORIGINS = [
425
- "https://app.onlook.ai",
1021
+ "https://app.onlook.com",
426
1022
  "http://localhost:3000",
427
1023
  "http://localhost:6006"
428
1024
  ];
429
- var AUTO_STORIES_FOLDER = ".onlook-stories";
430
- var storyRuntimeErrors = /* @__PURE__ */ new Map();
431
- var serveMetadataAndScreenshots = (req, res, next) => {
432
- if (req.url === "/onbook-health.json") {
433
- const cacheBuster = Date.now();
434
- fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
435
- cache: "no-store"
436
- }).then((response) => response.json()).then((indexData) => {
437
- const entries = indexData.entries || {};
438
- const autoStories = [];
439
- const userStories = [];
440
- for (const [storyId, entry] of Object.entries(entries)) {
441
- const importPath = entry.importPath || "";
442
- if (importPath.includes(AUTO_STORIES_FOLDER)) {
443
- autoStories.push(storyId);
444
- } else {
445
- userStories.push(storyId);
446
- }
447
- }
448
- const healthy = autoStories.filter((id) => !storyRuntimeErrors.has(id));
449
- const broken = autoStories.filter((id) => storyRuntimeErrors.has(id)).map((id) => ({
450
- storyId: id,
451
- // biome-ignore lint/style/noNonNullAssertion: filtered above
452
- error: storyRuntimeErrors.get(id)
453
- }));
454
- res.setHeader("Content-Type", "application/json");
455
- res.setHeader("Access-Control-Allow-Origin", "*");
456
- res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
457
- res.end(
458
- JSON.stringify({
459
- autoStoryCount: autoStories.length,
460
- userStoryCount: userStories.length,
461
- healthy,
462
- broken
463
- })
464
- );
465
- }).catch((error) => {
466
- res.statusCode = 500;
467
- res.setHeader("Content-Type", "application/json");
468
- res.end(
469
- JSON.stringify({
470
- error: "Failed to fetch story index",
471
- details: String(error)
472
- })
473
- );
1025
+ var manifestCache = null;
1026
+ var manifestHash = null;
1027
+ function readManifestFromDisk(filePath) {
1028
+ try {
1029
+ if (!fs5.existsSync(filePath)) return null;
1030
+ const raw = fs5.readFileSync(filePath, "utf-8");
1031
+ const hash = createHash("sha1").update(raw).digest("hex");
1032
+ const manifest = JSON.parse(raw);
1033
+ return { manifest, hash };
1034
+ } catch (err) {
1035
+ console.warn("[STORYBOOK_PLUGIN] Failed to read manifest", {
1036
+ filePath,
1037
+ error: err instanceof Error ? err.message : String(err)
474
1038
  });
475
- return;
1039
+ return null;
476
1040
  }
477
- if (req.url === "/onbook-report-error" && req.method === "POST") {
478
- let body = "";
479
- req.on("data", (chunk) => {
480
- body += chunk.toString();
481
- });
482
- req.on("end", () => {
483
- try {
484
- const { storyId, error } = JSON.parse(body);
485
- if (storyId && error) {
486
- storyRuntimeErrors.set(storyId, error);
487
- console.log(
488
- `[STORYBOOK_PLUGIN] Story runtime error reported: ${storyId}`,
489
- error
490
- );
491
- }
492
- res.setHeader("Access-Control-Allow-Origin", "*");
493
- res.statusCode = 200;
494
- res.end("ok");
495
- } catch {
496
- res.statusCode = 400;
497
- res.end("Invalid JSON");
498
- }
499
- });
500
- return;
1041
+ }
1042
+ function refreshManifest(filePath) {
1043
+ const result = readManifestFromDisk(filePath);
1044
+ if (!result) {
1045
+ return false;
1046
+ }
1047
+ if (result.hash === manifestHash) return false;
1048
+ manifestCache = result.manifest;
1049
+ manifestHash = result.hash;
1050
+ console.log("[STORYBOOK_PLUGIN] Manifest cache refreshed", {
1051
+ filePath,
1052
+ hash: result.hash.slice(0, 8),
1053
+ storyCount: Object.keys(result.manifest.stories ?? {}).length
1054
+ });
1055
+ return true;
1056
+ }
1057
+ var TAILWIND_ENTRY_CANDIDATES = [
1058
+ "src/app/globals.css",
1059
+ "app/globals.css",
1060
+ "src/index.css",
1061
+ "src/main.css",
1062
+ "src/styles.css",
1063
+ "src/styles/globals.css",
1064
+ "src/styles/index.css",
1065
+ "styles/globals.css",
1066
+ "styles/index.css",
1067
+ "app.css",
1068
+ "index.css"
1069
+ ];
1070
+ var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
1071
+ function detectTailwindEntryCss(cwd) {
1072
+ for (const rel of TAILWIND_ENTRY_CANDIDATES) {
1073
+ const abs = path7.join(cwd, rel);
1074
+ try {
1075
+ if (!fs5.existsSync(abs)) continue;
1076
+ const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
1077
+ if (TAILWIND_DIRECTIVE_PATTERN.test(head)) return abs;
1078
+ } catch {
1079
+ }
501
1080
  }
1081
+ return null;
1082
+ }
1083
+ var serveMetadataAndScreenshots = (req, res, next) => {
502
1084
  if (req.url === "/onbook-index.json") {
503
1085
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
504
- const manifestPath = path.join(process.cwd(), ".storybook-cache", "manifest.json");
505
1086
  const cacheBuster = Date.now();
506
1087
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
507
1088
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -518,7 +1099,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
518
1099
  });
519
1100
  return response.json();
520
1101
  }).then((indexData) => {
521
- const manifest = fs5.existsSync(manifestPath) ? JSON.parse(fs5.readFileSync(manifestPath, "utf-8")) : { stories: {} };
1102
+ const manifest = manifestCache ?? { stories: {} };
522
1103
  const defaultBoundingBox = { width: 1920, height: 1080 };
523
1104
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
524
1105
  const manifestEntry = manifest.stories?.[storyId];
@@ -585,8 +1166,42 @@ var serveMetadataAndScreenshots = (req, res, next) => {
585
1166
  });
586
1167
  return;
587
1168
  }
1169
+ if (req.url?.startsWith("/api/console-logs")) {
1170
+ const url = new URL(req.url, "http://localhost");
1171
+ const storyId = url.searchParams.get("storyId");
1172
+ const theme = url.searchParams.get("theme") || "light";
1173
+ if (!storyId) {
1174
+ res.statusCode = 400;
1175
+ res.setHeader("Content-Type", "application/json");
1176
+ res.end(JSON.stringify({ error: "storyId is required" }));
1177
+ return;
1178
+ }
1179
+ if (theme !== "light" && theme !== "dark") {
1180
+ res.statusCode = 400;
1181
+ res.setHeader("Content-Type", "application/json");
1182
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1183
+ return;
1184
+ }
1185
+ captureConsoleLogs(storyId, theme).then((result) => {
1186
+ res.setHeader("Content-Type", "application/json");
1187
+ res.setHeader("Access-Control-Allow-Origin", "*");
1188
+ res.setHeader("Cache-Control", "no-cache");
1189
+ res.end(JSON.stringify(result));
1190
+ }).catch((error) => {
1191
+ console.error("Console log capture error:", error);
1192
+ res.statusCode = 500;
1193
+ res.setHeader("Content-Type", "application/json");
1194
+ res.end(
1195
+ JSON.stringify({
1196
+ error: "Failed to capture console logs",
1197
+ details: String(error)
1198
+ })
1199
+ );
1200
+ });
1201
+ return;
1202
+ }
588
1203
  if (req.url?.startsWith("/screenshots/")) {
589
- const screenshotPath = path.join(
1204
+ const screenshotPath = path7.join(
590
1205
  process.cwd(),
591
1206
  ".storybook-cache",
592
1207
  req.url.replace("/screenshots/", "screenshots/")
@@ -607,7 +1222,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
607
1222
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
608
1223
  );
609
1224
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
610
- const storyDir = path.join(
1225
+ const storyDir = path7.join(
611
1226
  process.cwd(),
612
1227
  ".storybook-cache",
613
1228
  "screenshots",
@@ -628,6 +1243,8 @@ var serveMetadataAndScreenshots = (req, res, next) => {
628
1243
  );
629
1244
  res.statusCode = 500;
630
1245
  res.setHeader("Content-Type", "application/json");
1246
+ res.setHeader("Access-Control-Allow-Origin", "*");
1247
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
631
1248
  res.end(
632
1249
  JSON.stringify({
633
1250
  error: "Failed to generate screenshot",
@@ -638,11 +1255,41 @@ var serveMetadataAndScreenshots = (req, res, next) => {
638
1255
  return;
639
1256
  }
640
1257
  res.statusCode = 404;
1258
+ res.setHeader("Access-Control-Allow-Origin", "*");
1259
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
641
1260
  res.end("Screenshot not found");
642
1261
  return;
643
1262
  }
644
1263
  next();
645
1264
  };
1265
+ var ONLOOK_PLUGIN_CLAIMS_GLOBAL = "__ONLOOK_STORYBOOK_PLUGIN_CLAIMS__";
1266
+ function claimRegistry() {
1267
+ const g = globalThis;
1268
+ let registry = g[ONLOOK_PLUGIN_CLAIMS_GLOBAL];
1269
+ if (!registry) {
1270
+ registry = /* @__PURE__ */ new WeakMap();
1271
+ g[ONLOOK_PLUGIN_CLAIMS_GLOBAL] = registry;
1272
+ }
1273
+ return registry;
1274
+ }
1275
+ function withDedupeApply(plugins, instanceId) {
1276
+ for (const plugin of plugins) {
1277
+ const prior = plugin.apply;
1278
+ plugin.apply = (config, env) => {
1279
+ const priorOk = prior === void 0 ? true : typeof prior === "string" ? env.command === prior : prior(config, env);
1280
+ if (!priorOk) return false;
1281
+ if (env === null || typeof env !== "object") return true;
1282
+ const registry = claimRegistry();
1283
+ const claimed = registry.get(env);
1284
+ if (claimed === void 0) {
1285
+ registry.set(env, instanceId);
1286
+ return true;
1287
+ }
1288
+ return claimed === instanceId;
1289
+ };
1290
+ }
1291
+ return plugins;
1292
+ }
646
1293
  function storybookOnlookPlugin(options = {}) {
647
1294
  if (process.env.CHROMATIC || process.env.CI) {
648
1295
  console.log("[STORYBOOK_PLUGIN] Disabled in CI/Chromatic environment");
@@ -650,25 +1297,38 @@ function storybookOnlookPlugin(options = {}) {
650
1297
  }
651
1298
  const port = options.port ?? 6006;
652
1299
  const allowedOrigins = [...DEFAULT_ALLOWED_ORIGINS, ...options.allowedOrigins ?? []];
1300
+ const hmrOverride = options.hmr;
653
1301
  console.log("[STORYBOOK_PLUGIN] Plugin initialized", {
654
1302
  port,
655
1303
  allowedOrigins,
656
1304
  storybookLocation,
657
- repoRoot
1305
+ repoRoot,
1306
+ hmr: hmrOverride ?? null
658
1307
  });
1308
+ const aliasEntries = options.resolveAliases === false ? [] : resolveTsconfigAliases(process.cwd());
1309
+ if (aliasEntries.length > 0) {
1310
+ console.log("[STORYBOOK_PLUGIN] Resolved tsconfig path aliases", {
1311
+ count: aliasEntries.length,
1312
+ aliases: aliasEntries.map((a) => a.find.source)
1313
+ });
1314
+ }
659
1315
  const mainPlugin = {
660
1316
  name: "storybook-onlook-plugin",
661
1317
  config() {
662
1318
  return {
1319
+ ...aliasEntries.length > 0 && {
1320
+ resolve: { alias: aliasEntries }
1321
+ },
663
1322
  server: {
664
- // E2B sandbox HMR configuration
665
- hmr: {
666
- // E2B sandboxes use HTTPS, so we need secure WebSocket
667
- protocol: "wss",
668
- // E2B routes through standard HTTPS port 443
669
- clientPort: 443,
670
- // The actual Storybook server port inside the sandbox
671
- port
1323
+ // HMR override applies only when the caller explicitly opts in
1324
+ // (typically: Storybook fronted over HTTPS by a reverse proxy).
1325
+ // Locally, omitting this lets Vite use defaults
1326
+ // (ws://localhost:<port>) and HMR works as expected.
1327
+ ...hmrOverride && {
1328
+ hmr: {
1329
+ ...hmrOverride,
1330
+ port
1331
+ }
672
1332
  },
673
1333
  cors: {
674
1334
  origin: allowedOrigins
@@ -679,60 +1339,112 @@ function storybookOnlookPlugin(options = {}) {
679
1339
  configureServer(server) {
680
1340
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
681
1341
  server.middlewares.use(serveMetadataAndScreenshots);
1342
+ const cachedManifestPath = path7.join(
1343
+ process.cwd(),
1344
+ ".storybook-cache",
1345
+ "manifest.json"
1346
+ );
1347
+ refreshManifest(cachedManifestPath);
1348
+ let pending2;
1349
+ const scheduleRefresh = (file) => {
1350
+ if (file !== cachedManifestPath) return;
1351
+ if (pending2) clearTimeout(pending2);
1352
+ pending2 = setTimeout(() => {
1353
+ pending2 = void 0;
1354
+ refreshManifest(cachedManifestPath);
1355
+ }, 100);
1356
+ };
1357
+ server.watcher.on("add", scheduleRefresh);
1358
+ server.watcher.on("change", scheduleRefresh);
1359
+ const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1360
+ const resolvedStoryGlobs = storyGlobs.map(
1361
+ (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1362
+ );
1363
+ if (resolvedStoryGlobs.length > 0) {
1364
+ server.watcher.add(resolvedStoryGlobs);
1365
+ console.log("[STORYBOOK_PLUGIN] Extended watcher coverage", {
1366
+ storyGlobs: resolvedStoryGlobs
1367
+ });
1368
+ }
1369
+ const tailwindEntryPath = (() => {
1370
+ if (options.tailwindEntryCss === false) return null;
1371
+ if (typeof options.tailwindEntryCss === "string") {
1372
+ return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1373
+ }
1374
+ return detectTailwindEntryCss(process.cwd());
1375
+ })();
1376
+ if (tailwindEntryPath) {
1377
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan watcher enabled", {
1378
+ entry: tailwindEntryPath,
1379
+ autoDetected: options.tailwindEntryCss === void 0
1380
+ });
1381
+ const seenDirs = /* @__PURE__ */ new Set();
1382
+ let watcherReady = false;
1383
+ server.watcher.once("ready", () => {
1384
+ watcherReady = true;
1385
+ });
1386
+ server.watcher.on("add", (file) => {
1387
+ if (!/\.tsx?$/.test(file)) return;
1388
+ const dir = path7.dirname(file);
1389
+ const isNewDir = !seenDirs.has(dir);
1390
+ seenDirs.add(dir);
1391
+ if (!watcherReady || !isNewDir) return;
1392
+ try {
1393
+ const now = /* @__PURE__ */ new Date();
1394
+ fs5.utimesSync(tailwindEntryPath, now, now);
1395
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan for new dir", {
1396
+ dir,
1397
+ entry: tailwindEntryPath
1398
+ });
1399
+ } catch (err) {
1400
+ console.warn("[STORYBOOK_PLUGIN] Tailwind rescan: utimes failed", {
1401
+ entry: tailwindEntryPath,
1402
+ error: err instanceof Error ? err.message : String(err)
1403
+ });
1404
+ }
1405
+ });
1406
+ }
682
1407
  server.httpServer?.once("listening", () => {
1408
+ const probePort = server.config.server.port ?? port;
683
1409
  console.log("[STORYBOOK_PLUGIN] Server is listening", {
684
- port: server.config.server.port,
1410
+ port: probePort,
685
1411
  host: server.config.server.host
686
1412
  });
1413
+ void waitForIndexAndBroadcast(probePort);
687
1414
  });
688
1415
  },
689
1416
  configurePreviewServer(server) {
690
1417
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
691
1418
  server.middlewares.use(serveMetadataAndScreenshots);
1419
+ refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
692
1420
  },
693
- handleHotUpdate(ctx) {
694
- if (ctx.file.includes(AUTO_STORIES_FOLDER) && ctx.file.endsWith(".stories.tsx")) {
695
- enrichStoryFile(ctx.file);
696
- }
697
- return handleStoryFileChange(ctx);
698
- }
1421
+ handleHotUpdate: handleStoryFileChange
699
1422
  };
700
- const plugins = [componentLocPlugin(), mainPlugin];
701
- if (options.autoStories !== false) {
702
- const imports = options.autoStories ?? ["src/**/*.tsx"];
703
- const ignores = options.autoStoriesIgnore ?? [
704
- "src/**/*.stories.tsx",
705
- "src/**/*.stories.ts",
706
- "src/**/*.test.tsx",
707
- "src/**/*.test.ts",
708
- "src/**/*.spec.tsx",
709
- "src/**/*.spec.ts",
710
- "node_modules/**",
711
- "**/.onlook-stories/**"
712
- ];
713
- console.log("[STORYBOOK_PLUGIN] Auto-story generation enabled", {
714
- imports,
715
- ignores,
716
- storiesFolder: AUTO_STORIES_FOLDER
717
- });
1423
+ const envContainment = envContainmentPlugin(options.envContainment);
1424
+ const moduleLoadCatchAll = options.moduleLoadCatchAll === false ? null : moduleLoadCatchAllPlugin();
1425
+ return withDedupeApply(
1426
+ [
1427
+ ...envContainment ? [envContainment] : [],
1428
+ ...moduleLoadCatchAll ? [moduleLoadCatchAll] : [],
1429
+ componentLocPlugin(),
1430
+ mainPlugin,
1431
+ fastRefreshTolerantExportsPlugin(),
1432
+ softStoryRerenderPlugin()
1433
+ ],
1434
+ /* @__PURE__ */ Symbol("onlook-storybook-plugin-instance")
1435
+ );
1436
+ }
1437
+ var tolerantCsfIndexer = {
1438
+ test: /(stories|story)\.(m?js|ts)x?$/,
1439
+ createIndex: async (fileName, options) => {
718
1440
  try {
719
- plugins.push(
720
- autoStoryGenerator.vite({
721
- preset: "react",
722
- imports,
723
- ignores,
724
- storiesFolder: AUTO_STORIES_FOLDER,
725
- isGenerateStoriesFileAtBuild: true
726
- })
727
- );
1441
+ return (await readCsf(fileName, options)).parse().indexInputs;
728
1442
  } catch (err) {
729
- console.error(
730
- "[STORYBOOK_PLUGIN] ASG plugin failed to initialize, continuing without auto-stories",
731
- err
732
- );
1443
+ const msg = err instanceof Error ? err.message : String(err);
1444
+ console.warn("[onbook] Skipping broken story file:", fileName, msg);
1445
+ return [];
733
1446
  }
734
1447
  }
735
- return plugins;
736
- }
1448
+ };
737
1449
 
738
- export { AUTO_STORIES_FOLDER, storybookOnlookPlugin };
1450
+ export { MODULE_LOAD_CATCH_ALL_PLUGIN_NAME, ONLOOK_CONTAINED_CARD_ATTR, ONLOOK_CONTAINED_EVENT, ONLOOK_CONTAINED_MODULES_GLOBAL, ONLOOK_CONTAINED_PROP, ONLOOK_CONTAINMENT_CONTRACT_GLOBAL, ONLOOK_CONTAINMENT_CONTRACT_VERSION, ONLOOK_RENDERED_EVENT, ONLOOK_RENDER_STATE_ATTR, ONLOOK_RENDER_STORY_ATTR, ONLOOK_UNCONTAINED_ERROR_EVENT, RESOLVED_STORYBOOK_VIRTUAL_STORIES_ID, STORYBOOK_VIRTUAL_STORIES_ID, envContainmentPlugin, moduleLoadCatchAllPlugin, storybookOnlookPlugin, tolerantCsfIndexer };