@onlook/storybook-plugin 0.4.0-beta.0 → 0.4.0-beta.10

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 fs5, { existsSync } from 'fs';
2
- import path, { dirname, join, relative } from 'path';
1
+ import { createRequire } from 'node:module';
2
+ import path6, { dirname, join, relative } from 'path';
3
+ import crypto, { createHash } from 'crypto';
4
+ import fs4, { existsSync } from 'fs';
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(path6.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 (path6.isAbsolute(cleaned)) {
100
+ direct = cleaned;
101
+ } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
102
+ direct = path6.resolve(path6.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 ? path6.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
+ };
132
+ }
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");
67
191
  }
68
- function enrichStoryFile(storyFilePath) {
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;
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;
94
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 = path6.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 = path6.join(process.cwd(), ".storybook-cache");
391
+ var SCREENSHOTS_DIR = path6.join(CACHE_DIR, "screenshots");
392
+ var MANIFEST_PATH = path6.join(CACHE_DIR, "manifest.json");
158
393
  var VIEWPORT_WIDTH = 1920;
159
394
  var VIEWPORT_HEIGHT = 1080;
160
395
  var MIN_COMPONENT_WIDTH = 420;
@@ -162,30 +397,30 @@ var MIN_COMPONENT_HEIGHT = 280;
162
397
 
163
398
  // src/utils/fileSystem/fileSystem.ts
164
399
  function ensureCacheDirectories() {
165
- if (!fs5.existsSync(CACHE_DIR)) {
166
- fs5.mkdirSync(CACHE_DIR, { recursive: true });
400
+ if (!fs4.existsSync(CACHE_DIR)) {
401
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
167
402
  }
168
- if (!fs5.existsSync(SCREENSHOTS_DIR)) {
169
- fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
403
+ if (!fs4.existsSync(SCREENSHOTS_DIR)) {
404
+ fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
170
405
  }
171
406
  }
172
407
  function computeFileHash(filePath) {
173
- if (!fs5.existsSync(filePath)) {
408
+ if (!fs4.existsSync(filePath)) {
174
409
  return "";
175
410
  }
176
- const content = fs5.readFileSync(filePath, "utf-8");
411
+ const content = fs4.readFileSync(filePath, "utf-8");
177
412
  return crypto.createHash("sha256").update(content).digest("hex");
178
413
  }
179
414
  function loadManifest() {
180
- if (fs5.existsSync(MANIFEST_PATH)) {
181
- const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
415
+ if (fs4.existsSync(MANIFEST_PATH)) {
416
+ const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
182
417
  return JSON.parse(content);
183
418
  }
184
419
  return { stories: {} };
185
420
  }
186
421
  function saveManifest(manifest) {
187
422
  ensureCacheDirectories();
188
- fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
423
+ fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
189
424
  }
190
425
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
191
426
  const manifest = loadManifest();
@@ -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 = path6.join(SCREENSHOTS_DIR, storyId);
514
+ return path6.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,9 +598,9 @@ 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);
303
- if (!fs5.existsSync(storyDir)) {
304
- fs5.mkdirSync(storyDir, { recursive: true });
601
+ const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
602
+ if (!fs4.existsSync(storyDir)) {
603
+ fs4.mkdirSync(storyDir, { recursive: true });
305
604
  }
306
605
  const screenshotPath = getScreenshotPath(storyId, theme);
307
606
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -312,7 +611,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
312
611
  storybookUrl,
313
612
  timeoutMs
314
613
  );
315
- fs5.writeFileSync(screenshotPath, buffer);
614
+ fs4.writeFileSync(screenshotPath, buffer);
316
615
  return { path: screenshotPath, boundingBox };
317
616
  } catch (error) {
318
617
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -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 = path6.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,123 @@ 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, path7) => {
895
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path7}${quote}`);
896
+ });
897
+ return out === code ? null : { code: out, map: null };
898
+ }
899
+ };
900
+ }
407
901
  function findGitRoot(startPath) {
408
902
  let currentPath = startPath;
409
903
  while (currentPath !== dirname(currentPath)) {
@@ -422,86 +916,71 @@ var gitRoot = findGitRoot(storybookDir);
422
916
  var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
423
917
  var repoRoot = gitRoot || process.cwd();
424
918
  var DEFAULT_ALLOWED_ORIGINS = [
425
- "https://app.onlook.ai",
919
+ "https://app.onlook.com",
426
920
  "http://localhost:3000",
427
921
  "http://localhost:6006"
428
922
  ];
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
- );
923
+ var manifestCache = null;
924
+ var manifestHash = null;
925
+ function readManifestFromDisk(filePath) {
926
+ try {
927
+ if (!fs4.existsSync(filePath)) return null;
928
+ const raw = fs4.readFileSync(filePath, "utf-8");
929
+ const hash = createHash("sha1").update(raw).digest("hex");
930
+ const manifest = JSON.parse(raw);
931
+ return { manifest, hash };
932
+ } catch (err) {
933
+ console.warn("[STORYBOOK_PLUGIN] Failed to read manifest", {
934
+ filePath,
935
+ error: err instanceof Error ? err.message : String(err)
474
936
  });
475
- return;
937
+ return null;
476
938
  }
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;
939
+ }
940
+ function refreshManifest(filePath) {
941
+ const result = readManifestFromDisk(filePath);
942
+ if (!result) {
943
+ return false;
944
+ }
945
+ if (result.hash === manifestHash) return false;
946
+ manifestCache = result.manifest;
947
+ manifestHash = result.hash;
948
+ console.log("[STORYBOOK_PLUGIN] Manifest cache refreshed", {
949
+ filePath,
950
+ hash: result.hash.slice(0, 8),
951
+ storyCount: Object.keys(result.manifest.stories ?? {}).length
952
+ });
953
+ return true;
954
+ }
955
+ var TAILWIND_ENTRY_CANDIDATES = [
956
+ "src/app/globals.css",
957
+ "app/globals.css",
958
+ "src/index.css",
959
+ "src/main.css",
960
+ "src/styles.css",
961
+ "src/styles/globals.css",
962
+ "src/styles/index.css",
963
+ "styles/globals.css",
964
+ "styles/index.css",
965
+ "app.css",
966
+ "index.css"
967
+ ];
968
+ var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
969
+ function detectTailwindEntryCss(cwd) {
970
+ for (const rel of TAILWIND_ENTRY_CANDIDATES) {
971
+ const abs = path6.join(cwd, rel);
972
+ try {
973
+ if (!fs4.existsSync(abs)) continue;
974
+ const head = fs4.readFileSync(abs, "utf-8").slice(0, 2048);
975
+ if (TAILWIND_DIRECTIVE_PATTERN.test(head)) return abs;
976
+ } catch {
977
+ }
501
978
  }
979
+ return null;
980
+ }
981
+ var serveMetadataAndScreenshots = (req, res, next) => {
502
982
  if (req.url === "/onbook-index.json") {
503
983
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
504
- const manifestPath = path.join(process.cwd(), ".storybook-cache", "manifest.json");
505
984
  const cacheBuster = Date.now();
506
985
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
507
986
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -518,7 +997,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
518
997
  });
519
998
  return response.json();
520
999
  }).then((indexData) => {
521
- const manifest = fs5.existsSync(manifestPath) ? JSON.parse(fs5.readFileSync(manifestPath, "utf-8")) : { stories: {} };
1000
+ const manifest = manifestCache ?? { stories: {} };
522
1001
  const defaultBoundingBox = { width: 1920, height: 1080 };
523
1002
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
524
1003
  const manifestEntry = manifest.stories?.[storyId];
@@ -585,8 +1064,42 @@ var serveMetadataAndScreenshots = (req, res, next) => {
585
1064
  });
586
1065
  return;
587
1066
  }
1067
+ if (req.url?.startsWith("/api/console-logs")) {
1068
+ const url = new URL(req.url, "http://localhost");
1069
+ const storyId = url.searchParams.get("storyId");
1070
+ const theme = url.searchParams.get("theme") || "light";
1071
+ if (!storyId) {
1072
+ res.statusCode = 400;
1073
+ res.setHeader("Content-Type", "application/json");
1074
+ res.end(JSON.stringify({ error: "storyId is required" }));
1075
+ return;
1076
+ }
1077
+ if (theme !== "light" && theme !== "dark") {
1078
+ res.statusCode = 400;
1079
+ res.setHeader("Content-Type", "application/json");
1080
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1081
+ return;
1082
+ }
1083
+ captureConsoleLogs(storyId, theme).then((result) => {
1084
+ res.setHeader("Content-Type", "application/json");
1085
+ res.setHeader("Access-Control-Allow-Origin", "*");
1086
+ res.setHeader("Cache-Control", "no-cache");
1087
+ res.end(JSON.stringify(result));
1088
+ }).catch((error) => {
1089
+ console.error("Console log capture error:", error);
1090
+ res.statusCode = 500;
1091
+ res.setHeader("Content-Type", "application/json");
1092
+ res.end(
1093
+ JSON.stringify({
1094
+ error: "Failed to capture console logs",
1095
+ details: String(error)
1096
+ })
1097
+ );
1098
+ });
1099
+ return;
1100
+ }
588
1101
  if (req.url?.startsWith("/screenshots/")) {
589
- const screenshotPath = path.join(
1102
+ const screenshotPath = path6.join(
590
1103
  process.cwd(),
591
1104
  ".storybook-cache",
592
1105
  req.url.replace("/screenshots/", "screenshots/")
@@ -595,11 +1108,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
595
1108
  const storyId = urlParts[0];
596
1109
  const themeFile = urlParts[1];
597
1110
  const theme = themeFile?.replace(".png", "");
598
- if (fs5.existsSync(screenshotPath)) {
1111
+ if (fs4.existsSync(screenshotPath)) {
599
1112
  res.setHeader("Content-Type", "image/png");
600
1113
  res.setHeader("Access-Control-Allow-Origin", "*");
601
1114
  res.setHeader("Cache-Control", "public, max-age=3600");
602
- fs5.createReadStream(screenshotPath).pipe(res);
1115
+ fs4.createReadStream(screenshotPath).pipe(res);
603
1116
  return;
604
1117
  }
605
1118
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -607,16 +1120,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
607
1120
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
608
1121
  );
609
1122
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
610
- const storyDir = path.join(
1123
+ const storyDir = path6.join(
611
1124
  process.cwd(),
612
1125
  ".storybook-cache",
613
1126
  "screenshots",
614
1127
  storyId
615
1128
  );
616
- if (!fs5.existsSync(storyDir)) {
617
- fs5.mkdirSync(storyDir, { recursive: true });
1129
+ if (!fs4.existsSync(storyDir)) {
1130
+ fs4.mkdirSync(storyDir, { recursive: true });
618
1131
  }
619
- fs5.writeFileSync(screenshotPath, buffer);
1132
+ fs4.writeFileSync(screenshotPath, buffer);
620
1133
  res.setHeader("Content-Type", "image/png");
621
1134
  res.setHeader("Access-Control-Allow-Origin", "*");
622
1135
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -628,6 +1141,8 @@ var serveMetadataAndScreenshots = (req, res, next) => {
628
1141
  );
629
1142
  res.statusCode = 500;
630
1143
  res.setHeader("Content-Type", "application/json");
1144
+ res.setHeader("Access-Control-Allow-Origin", "*");
1145
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
631
1146
  res.end(
632
1147
  JSON.stringify({
633
1148
  error: "Failed to generate screenshot",
@@ -638,11 +1153,41 @@ var serveMetadataAndScreenshots = (req, res, next) => {
638
1153
  return;
639
1154
  }
640
1155
  res.statusCode = 404;
1156
+ res.setHeader("Access-Control-Allow-Origin", "*");
1157
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
641
1158
  res.end("Screenshot not found");
642
1159
  return;
643
1160
  }
644
1161
  next();
645
1162
  };
1163
+ var ONLOOK_PLUGIN_CLAIMS_GLOBAL = "__ONLOOK_STORYBOOK_PLUGIN_CLAIMS__";
1164
+ function claimRegistry() {
1165
+ const g = globalThis;
1166
+ let registry = g[ONLOOK_PLUGIN_CLAIMS_GLOBAL];
1167
+ if (!registry) {
1168
+ registry = /* @__PURE__ */ new WeakMap();
1169
+ g[ONLOOK_PLUGIN_CLAIMS_GLOBAL] = registry;
1170
+ }
1171
+ return registry;
1172
+ }
1173
+ function withDedupeApply(plugins, instanceId) {
1174
+ for (const plugin of plugins) {
1175
+ const prior = plugin.apply;
1176
+ plugin.apply = (config, env) => {
1177
+ const priorOk = prior === void 0 ? true : typeof prior === "string" ? env.command === prior : prior(config, env);
1178
+ if (!priorOk) return false;
1179
+ if (env === null || typeof env !== "object") return true;
1180
+ const registry = claimRegistry();
1181
+ const claimed = registry.get(env);
1182
+ if (claimed === void 0) {
1183
+ registry.set(env, instanceId);
1184
+ return true;
1185
+ }
1186
+ return claimed === instanceId;
1187
+ };
1188
+ }
1189
+ return plugins;
1190
+ }
646
1191
  function storybookOnlookPlugin(options = {}) {
647
1192
  if (process.env.CHROMATIC || process.env.CI) {
648
1193
  console.log("[STORYBOOK_PLUGIN] Disabled in CI/Chromatic environment");
@@ -650,25 +1195,28 @@ function storybookOnlookPlugin(options = {}) {
650
1195
  }
651
1196
  const port = options.port ?? 6006;
652
1197
  const allowedOrigins = [...DEFAULT_ALLOWED_ORIGINS, ...options.allowedOrigins ?? []];
1198
+ const hmrOverride = options.hmr;
653
1199
  console.log("[STORYBOOK_PLUGIN] Plugin initialized", {
654
1200
  port,
655
1201
  allowedOrigins,
656
1202
  storybookLocation,
657
- repoRoot
1203
+ repoRoot,
1204
+ hmr: hmrOverride ?? null
658
1205
  });
659
1206
  const mainPlugin = {
660
1207
  name: "storybook-onlook-plugin",
661
1208
  config() {
662
1209
  return {
663
1210
  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
1211
+ // HMR override applies only when the caller explicitly opts in
1212
+ // (typically: Storybook fronted over HTTPS by a reverse proxy).
1213
+ // Locally, omitting this lets Vite use defaults
1214
+ // (ws://localhost:<port>) and HMR works as expected.
1215
+ ...hmrOverride && {
1216
+ hmr: {
1217
+ ...hmrOverride,
1218
+ port
1219
+ }
672
1220
  },
673
1221
  cors: {
674
1222
  origin: allowedOrigins
@@ -679,60 +1227,112 @@ function storybookOnlookPlugin(options = {}) {
679
1227
  configureServer(server) {
680
1228
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
681
1229
  server.middlewares.use(serveMetadataAndScreenshots);
1230
+ const cachedManifestPath = path6.join(
1231
+ process.cwd(),
1232
+ ".storybook-cache",
1233
+ "manifest.json"
1234
+ );
1235
+ refreshManifest(cachedManifestPath);
1236
+ let pending2;
1237
+ const scheduleRefresh = (file) => {
1238
+ if (file !== cachedManifestPath) return;
1239
+ if (pending2) clearTimeout(pending2);
1240
+ pending2 = setTimeout(() => {
1241
+ pending2 = void 0;
1242
+ refreshManifest(cachedManifestPath);
1243
+ }, 100);
1244
+ };
1245
+ server.watcher.on("add", scheduleRefresh);
1246
+ server.watcher.on("change", scheduleRefresh);
1247
+ const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1248
+ const resolvedStoryGlobs = storyGlobs.map(
1249
+ (glob) => path6.isAbsolute(glob) ? glob : path6.join(process.cwd(), glob)
1250
+ );
1251
+ if (resolvedStoryGlobs.length > 0) {
1252
+ server.watcher.add(resolvedStoryGlobs);
1253
+ console.log("[STORYBOOK_PLUGIN] Extended watcher coverage", {
1254
+ storyGlobs: resolvedStoryGlobs
1255
+ });
1256
+ }
1257
+ const tailwindEntryPath = (() => {
1258
+ if (options.tailwindEntryCss === false) return null;
1259
+ if (typeof options.tailwindEntryCss === "string") {
1260
+ return path6.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path6.join(process.cwd(), options.tailwindEntryCss);
1261
+ }
1262
+ return detectTailwindEntryCss(process.cwd());
1263
+ })();
1264
+ if (tailwindEntryPath) {
1265
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan watcher enabled", {
1266
+ entry: tailwindEntryPath,
1267
+ autoDetected: options.tailwindEntryCss === void 0
1268
+ });
1269
+ const seenDirs = /* @__PURE__ */ new Set();
1270
+ let watcherReady = false;
1271
+ server.watcher.once("ready", () => {
1272
+ watcherReady = true;
1273
+ });
1274
+ server.watcher.on("add", (file) => {
1275
+ if (!/\.tsx?$/.test(file)) return;
1276
+ const dir = path6.dirname(file);
1277
+ const isNewDir = !seenDirs.has(dir);
1278
+ seenDirs.add(dir);
1279
+ if (!watcherReady || !isNewDir) return;
1280
+ try {
1281
+ const now = /* @__PURE__ */ new Date();
1282
+ fs4.utimesSync(tailwindEntryPath, now, now);
1283
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan for new dir", {
1284
+ dir,
1285
+ entry: tailwindEntryPath
1286
+ });
1287
+ } catch (err) {
1288
+ console.warn("[STORYBOOK_PLUGIN] Tailwind rescan: utimes failed", {
1289
+ entry: tailwindEntryPath,
1290
+ error: err instanceof Error ? err.message : String(err)
1291
+ });
1292
+ }
1293
+ });
1294
+ }
682
1295
  server.httpServer?.once("listening", () => {
1296
+ const probePort = server.config.server.port ?? port;
683
1297
  console.log("[STORYBOOK_PLUGIN] Server is listening", {
684
- port: server.config.server.port,
1298
+ port: probePort,
685
1299
  host: server.config.server.host
686
1300
  });
1301
+ void waitForIndexAndBroadcast(probePort);
687
1302
  });
688
1303
  },
689
1304
  configurePreviewServer(server) {
690
1305
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
691
1306
  server.middlewares.use(serveMetadataAndScreenshots);
1307
+ refreshManifest(path6.join(process.cwd(), ".storybook-cache", "manifest.json"));
692
1308
  },
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
- }
1309
+ handleHotUpdate: handleStoryFileChange
699
1310
  };
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
- });
1311
+ const envContainment = envContainmentPlugin(options.envContainment);
1312
+ const moduleLoadCatchAll = options.moduleLoadCatchAll === false ? null : moduleLoadCatchAllPlugin();
1313
+ return withDedupeApply(
1314
+ [
1315
+ ...envContainment ? [envContainment] : [],
1316
+ ...moduleLoadCatchAll ? [moduleLoadCatchAll] : [],
1317
+ componentLocPlugin(),
1318
+ mainPlugin,
1319
+ fastRefreshTolerantExportsPlugin(),
1320
+ softStoryRerenderPlugin()
1321
+ ],
1322
+ /* @__PURE__ */ Symbol("onlook-storybook-plugin-instance")
1323
+ );
1324
+ }
1325
+ var tolerantCsfIndexer = {
1326
+ test: /(stories|story)\.(m?js|ts)x?$/,
1327
+ createIndex: async (fileName, options) => {
718
1328
  try {
719
- plugins.push(
720
- autoStoryGenerator.vite({
721
- preset: "react",
722
- imports,
723
- ignores,
724
- storiesFolder: AUTO_STORIES_FOLDER,
725
- isGenerateStoriesFileAtBuild: true
726
- })
727
- );
1329
+ return (await readCsf(fileName, options)).parse().indexInputs;
728
1330
  } catch (err) {
729
- console.error(
730
- "[STORYBOOK_PLUGIN] ASG plugin failed to initialize, continuing without auto-stories",
731
- err
732
- );
1331
+ const msg = err instanceof Error ? err.message : String(err);
1332
+ console.warn("[onbook] Skipping broken story file:", fileName, msg);
1333
+ return [];
733
1334
  }
734
1335
  }
735
- return plugins;
736
- }
1336
+ };
737
1337
 
738
- export { AUTO_STORIES_FOLDER, storybookOnlookPlugin };
1338
+ 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 };