@onlook/storybook-plugin 0.4.0-beta.6 → 0.4.0-beta.9

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,671 +1,310 @@
1
- import fs2, { existsSync } from 'fs';
2
- import path4, { 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 { minimatch } from 'minimatch';
5
- import { Project, ts, TypeFlags } from 'ts-morph';
6
- import { glob } from 'glob';
7
- import { withDefaultConfig } from 'react-docgen-typescript';
8
6
  import generateModule from '@babel/generator';
9
7
  import { parse } from '@babel/parser';
10
8
  import traverseModule from '@babel/traverse';
11
9
  import * as t from '@babel/types';
12
- import crypto from 'crypto';
10
+ import { execFile } from 'child_process';
11
+ import { createRequire as createRequire$1 } from 'module';
12
+ import { promisify } from 'util';
13
13
  import { chromium } from 'playwright';
14
+ import { readCsf } from 'storybook/internal/csf-tools';
14
15
 
15
- // src/storybook-onlook-plugin.ts
16
- function getComponentInfo(componentDir, projectRootDir) {
17
- const fileParseInfo = path4.parse(componentDir);
18
- const prefixExtRegex = new RegExp(`(\\.\\w+)+(?=\\${fileParseInfo.ext}$)`);
19
- const prefixExtRegexMatch = fileParseInfo.base.match(prefixExtRegex);
20
- const prefixExt = prefixExtRegexMatch ? prefixExtRegexMatch[0] : void 0;
21
- const fileName = fileParseInfo.name.replace(prefixExt || "", "");
22
- const componentName = fileName === "index" ? fileParseInfo.dir.split("/").pop() : fileName;
23
- let relativeSourceFilePath = componentDir.replace(projectRootDir, "");
24
- if (relativeSourceFilePath.startsWith("/") || relativeSourceFilePath.startsWith("\\")) {
25
- relativeSourceFilePath = componentDir.replace(projectRootDir, "").slice(1);
26
- }
27
- return {
28
- fileBase: fileParseInfo.base,
29
- fileExt: fileParseInfo.ext,
30
- fileName,
31
- filePrefixExt: prefixExt,
32
- componentName,
33
- relativeSourceFilePath
34
- };
35
- }
36
- function pascalCase(str) {
37
- return str.replace(/[_\-.\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (_, c) => c.toUpperCase());
38
- }
39
- function removeQuotesAndWrapWithDoubleQuotes(str) {
40
- const quoteRemovedStr = str.replace(/"/g, "").replace(/'/g, "");
41
- return quoteRemovedStr;
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, "/");
42
35
  }
43
- function getTypeFlagsName(flags) {
44
- const keys = Object.keys(TypeFlags);
45
- const setFlags = keys.find((key) => flags === TypeFlags[key]);
46
- return setFlags || "err";
36
+ function stripQuery(id) {
37
+ const q = id.indexOf("?");
38
+ return q === -1 ? id : id.slice(0, q);
47
39
  }
48
- function getReactPropTypes({
49
- sourceFile,
50
- componentName
51
- }) {
52
- if (!componentName) {
53
- return {
54
- propTypes: void 0
55
- };
56
- }
57
- let propsPattern = "component-props";
58
- const pascalComponentName = pascalCase(componentName);
59
- const propsType = sourceFile.getTypeAlias(`${pascalComponentName}Props`);
60
- const propsInterface = sourceFile.getInterface(`${pascalComponentName}Props`);
61
- const propsOnlyType = sourceFile.getTypeAlias("Props");
62
- const propsOnlyInterface = sourceFile.getInterface("Props");
63
- const propsInline = sourceFile.getVariableDeclaration(pascalComponentName)?.getInitializerIfKindOrThrow(ts.SyntaxKind.ArrowFunction);
64
- const props = propsType?.getType() || propsInterface?.getType() || propsOnlyType?.getType() || propsOnlyInterface?.getType() || propsInline?.getParameters()[0]?.getType();
65
- if (!props) {
66
- return {
67
- propTypes: []
68
- };
69
- }
70
- let propsProperties = [];
71
- const isPropsIntersection = props.isIntersection();
72
- if (isPropsIntersection) {
73
- propsProperties = [];
74
- const intersectionTypes = props.getIntersectionTypes();
75
- intersectionTypes.forEach((intersectionType) => {
76
- const intersectionTypeText = intersectionType.getText();
77
- if (intersectionTypeText.includes("HTMLAttributes")) return;
78
- return propsProperties.push(...intersectionType.getProperties());
79
- });
80
- } else {
81
- propsProperties = props.getProperties();
82
- }
83
- if (propsOnlyType || propsOnlyInterface) propsPattern = "props";
84
- if (propsInline) propsPattern = "inline";
85
- const propTypes = propsProperties.map((prop) => {
86
- const propName = prop.getName();
87
- const propType = prop.getValueDeclaration()?.getType();
88
- if (!propType) {
89
- return {
90
- name: propName,
91
- type: ["err"],
92
- isOptional: prop.isOptional(),
93
- value: []
94
- };
95
- }
96
- if (propType.isUnion() && !propType.isBoolean()) {
97
- const unionTypes = propType.getUnionTypes();
98
- const type = Array.from(
99
- new Set(unionTypes.map((union) => getTypeFlagsName(union.getFlags().valueOf())))
100
- );
101
- return {
102
- name: propName,
103
- type,
104
- isOptional: prop.isOptional(),
105
- value: unionTypes.map(
106
- (union) => removeQuotesAndWrapWithDoubleQuotes(union.getText())
107
- )
108
- };
109
- }
110
- return {
111
- name: propName,
112
- type: [prop.getValueDeclaration().getType().getText()],
113
- isOptional: prop.isOptional(),
114
- value: []
115
- };
116
- });
117
- return {
118
- propsPattern,
119
- propTypes
120
- };
121
- }
122
- var errorDefinition = {
123
- // Common
124
- EC00: {
125
- title: "Unknown error",
126
- isCustomDetail: true
127
- },
128
- EC01: {
129
- title: "Not yet supported",
130
- detail: "This preset name is included in the preset bust not yet supported. Please wait for support.",
131
- isCustomDetail: false
132
- },
133
- EC02: {
134
- title: "Preset is not supported",
135
- isCustomDetail: true
136
- },
137
- EC03: {
138
- title: "Unable to get component name or file path correctly.",
139
- detail: "Please check the file path and try again.",
140
- isCustomDetail: false
141
- },
142
- EC04: {
143
- title: "Could not find argTypes",
144
- detail: "Error in genReactStoryFile.",
145
- isCustomDetail: false
146
- },
147
- EC05: {
148
- title: "Could not find meta",
149
- isCustomDetail: true
150
- },
151
- EC06: {
152
- title: "Could not find initializer",
153
- detail: "Could not get initializer of ObjectLiteralExpression.",
154
- isCustomDetail: false
155
- },
156
- EC07: {
157
- title: "Could not find component",
158
- isCustomDetail: true
159
- },
160
- EC08: {
161
- title: "Could not scan directory",
162
- isCustomDetail: true
163
- },
164
- EC09: {
165
- title: "Reading directory failed",
166
- isCustomDetail: true
167
- },
168
- EC10: {
169
- title: "Could not get property from stories",
170
- isCustomDetail: true
171
- },
172
- EC11: {
173
- title: "File is defective.",
174
- detail: "An error occurred during abstract syntax tree parsing.\nPlease check your file for problems.",
175
- isCustomDetail: false
176
- },
177
- // Lit
178
- EL01: {
179
- title: "Failed to save file",
180
- isCustomDetail: true
181
- }
182
- };
183
- var throwErr = (params) => {
184
- const { errorCode, detail } = params;
185
- const detailText = errorDefinition[errorCode].isCustomDetail ? detail : errorDefinition[errorCode].detail;
186
- console.error(`[ASG:${errorCode}] ${errorDefinition[errorCode].title}
187
- ${detailText}`);
188
- };
189
- function genReactStoryFile(input) {
190
- const {
191
- componentName,
192
- fileBase,
193
- fileName,
194
- filePrefixExt,
195
- relativeSourceFilePath,
196
- sourceFile,
197
- storiesFolder
198
- } = input;
199
- if (!componentName || !fileBase) {
200
- throwErr({ errorCode: "EC03" });
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) {
201
46
  return null;
202
47
  }
203
- const { propTypes } = getReactPropTypes({ sourceFile, componentName });
204
- const pascalComponentName = pascalCase(componentName);
205
- if (!propTypes) {
206
- throwErr({ errorCode: "EC04" });
207
- return null;
48
+ const byModule = /* @__PURE__ */ new Map();
49
+ for (const sub of substitutions) {
50
+ byModule.set(normalizeSlashes(sub.module).replace(/^\.\//, ""), sub);
208
51
  }
209
- const exportedDeclarations = sourceFile.getExportedDeclarations();
210
- let isDefaultExportComponent = false;
211
- exportedDeclarations.forEach((declaration, exportName) => {
212
- if (exportName === "default") {
213
- const defaultExportName = declaration[0]?.getSymbol()?.getName();
214
- isDefaultExportComponent = defaultExportName === pascalComponentName;
215
- }
216
- });
217
- const pathToComponent = storiesFolder ? "../" : "./";
218
- const importSuffix = filePrefixExt || "";
219
- const importStatement = isDefaultExportComponent ? `import ${pascalComponentName} from "${pathToComponent}${fileName}${importSuffix}";` : `import { ${pascalComponentName} } from "${pathToComponent}${fileName}${importSuffix}";`;
220
- const args = {};
221
- for (const prop of propTypes) {
222
- if (prop.isOptional) {
223
- args[prop.name] = "undefined";
224
- continue;
225
- }
226
- if (prop.type.includes("boolean")) {
227
- args[prop.name] = "false";
228
- } else if (prop.value.length > 0) {
229
- args[prop.name] = `"${prop.value[0]}"`;
230
- } else if (prop.type.includes("string") || prop.type.includes("String")) {
231
- args[prop.name] = `"${prop.name}"`;
232
- } else if (prop.type.includes("number") || prop.type.includes("Number")) {
233
- args[prop.name] = "0";
234
- } else {
235
- args[prop.name] = "undefined";
236
- }
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(/\.[^.]+$/, ""));
237
56
  }
238
- const argTypes = {};
239
- for (const prop of propTypes) {
240
- if (prop.type[0] === "boolean") {
241
- argTypes[prop.name] = '{ control: "boolean" }';
242
- } else if (prop.type[0] === "object") {
243
- argTypes[prop.name] = '{ control: "object" }';
244
- } else if (prop.value.length > 1) {
245
- const options = prop.value.map((v) => `"${v}"`).join(", ");
246
- argTypes[prop.name] = `{ control: "select", options: [${options}] }`;
247
- } else if (prop.type[0] === "string") {
248
- argTypes[prop.name] = '{ control: "text" }';
249
- } else if (prop.type[0] === "number") {
250
- argTypes[prop.name] = '{ control: "number" }';
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
+ }
71
+ }
251
72
  }
252
- }
253
- const argsStr = formatObjectEntries(args, " ");
254
- const argTypesStr = formatObjectEntries(argTypes, " ");
255
- const storyTitle = buildStoryTitle(relativeSourceFilePath, pascalComponentName);
256
- return `import * as _React from "react";
257
- import type { Meta, StoryObj } from "@storybook/react";
258
- ${importStatement}
259
-
260
- // Inline error boundary \u2014 catches render crashes from missing providers/context
261
- class _StoryErrorBoundary extends _React.Component<
262
- { children: _React.ReactNode },
263
- { error: Error | null }
264
- > {
265
- state: { error: Error | null } = { error: null };
266
- static getDerivedStateFromError(error: Error) { return { error }; }
267
- render() {
268
- if (this.state.error) {
269
- return (
270
- <div style={{ padding: 24, fontFamily: "system-ui", color: "#888", fontSize: 13 }}>
271
- <div style={{ fontWeight: 600, marginBottom: 8, color: "#c44" }}>
272
- Cannot render in isolation
273
- </div>
274
- <div style={{ fontSize: 12, opacity: 0.7 }}>
275
- {this.state.error.message}
276
- </div>
277
- </div>
278
- );
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;
88
+ }
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);
103
+ }
104
+ if (direct) {
105
+ const rel = matchTarget(direct);
106
+ if (rel) return ENV_CONTAINMENT_VIRTUAL_PREFIX + rel;
107
+ return null;
108
+ }
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;
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);
279
130
  }
280
- return this.props.children;
281
- }
131
+ };
282
132
  }
283
-
284
- const meta: Meta<typeof ${pascalComponentName}> = {
285
- title: "${storyTitle}",
286
- component: ${pascalComponentName},
287
- tags: ["autodocs"],
288
- args: {${argsStr}},
289
- argTypes: {${argTypesStr}},
290
- decorators: [
291
- (Story) => (
292
- <_StoryErrorBoundary>
293
- <_React.Suspense fallback={<div style={{ padding: 24, color: "#888" }}>Loading...</div>}>
294
- <Story />
295
- </_React.Suspense>
296
- </_StoryErrorBoundary>
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)});`
297
188
  ),
298
- ],
299
- };
300
-
301
- export default meta;
302
- type Story = StoryObj<typeof meta>;
303
-
304
- export const Primary: Story = {};
305
- `;
189
+ ""
190
+ ].join("\n");
306
191
  }
307
- function buildStoryTitle(relPath, componentName) {
308
- let p = relPath.replace(/^src\//, "");
309
- const lastSlash = p.lastIndexOf("/");
310
- if (lastSlash !== -1) {
311
- p = p.substring(0, lastSlash);
312
- } else {
313
- p = "";
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;
314
208
  }
315
- p = p.replace(/\(([^)]+)\)/g, "$1");
316
- p = p.replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
317
- return p ? `${p}/${componentName}` : componentName;
318
- }
319
- function formatObjectEntries(obj, indent) {
320
- const entries = Object.entries(obj);
321
- if (entries.length === 0) return "";
322
- const lines = entries.map(([key, value]) => `${indent}${key}: ${value},`);
323
- return `
324
- ${lines.join("\n")}
325
- `;
326
- }
327
- function createLightProject() {
328
- return new Project({
329
- compilerOptions: { allowJs: true },
330
- useInMemoryFileSystem: true
331
- });
332
- }
333
- async function genStoryFile({
334
- options,
335
- id,
336
- projectRootDir
337
- }) {
338
- if (id.includes(".stories")) return null;
339
- const {
340
- fileBase,
341
- fileName,
342
- fileExt,
343
- filePrefixExt,
344
- componentName,
345
- relativeSourceFilePath
346
- } = getComponentInfo(id, projectRootDir);
209
+ synthetic.cause = original;
347
210
  try {
348
- const sourceCode = fs2.readFileSync(id, "utf-8");
349
- const sourceProject = createLightProject();
350
- const sourceFile = sourceProject.createSourceFile(fileBase || "temp.tsx", sourceCode);
351
- if (options.preset !== "react") {
352
- throwErr({
353
- errorCode: "EC02",
354
- detail: `Preset ${options.preset} is not supported in this fork. Only "react" is supported.`
355
- });
356
- return;
357
- }
358
- const storyCode = genReactStoryFile({
359
- componentName,
360
- fileBase,
361
- fileName,
362
- path: id,
363
- fileExt,
364
- filePrefixExt,
365
- relativeSourceFilePath,
366
- sourceFile,
367
- storiesFolder: options.storiesFolder
368
- });
369
- if (!storyCode) return null;
370
- const parsed = path4.parse(id);
371
- const storyFileName = `${fileName}.stories.tsx`;
372
- let storiesFilePath;
373
- if (options.storiesFolder) {
374
- const storiesFolderPath = path4.join(parsed.dir, options.storiesFolder);
375
- fs2.mkdirSync(storiesFolderPath, { recursive: true });
376
- storiesFilePath = path4.join(storiesFolderPath, storyFileName);
377
- } else {
378
- storiesFilePath = path4.join(parsed.dir, storyFileName);
379
- }
380
- if (!fs2.existsSync(storiesFilePath)) {
381
- fs2.writeFileSync(storiesFilePath, storyCode);
382
- }
383
- return storiesFilePath;
384
- } catch (err) {
385
- console.warn(`[ASG] Failed to generate story for ${id}:`, err);
386
- return null;
211
+ console.error(
212
+ "[ONLOOK] Story module failed to load \u2014 serving contained synthetic stories.",
213
+ { importPath, error: original }
214
+ );
215
+ } catch {
387
216
  }
388
- }
389
- async function getAllFilePaths({
390
- patterns,
391
- ignorePatterns,
392
- projectRootDir
393
- }) {
394
- const fullPatterns = patterns.map(
395
- (p) => path4.join(projectRootDir, p).replace(/\\/g, "/")
396
- );
397
- const ignoreFullPatterns = ignorePatterns?.map(
398
- (p) => path4.join(projectRootDir, p).replace(/\\/g, "/")
399
- );
400
- const filePaths = await glob(fullPatterns, {
401
- ignore: ignoreFullPatterns,
402
- nodir: true
403
- });
404
- return filePaths.map((p) => p.replace(/\\/g, "/"));
405
- }
406
- var PLUGIN_NAME = "auto-story-generator";
407
- var DEFAULT_BATCH_SIZE = 50;
408
- var DEFAULT_CONCURRENCY = 8;
409
- var CACHE_FILE_NAME = ".asg-cache.json";
410
- var mtimeCache = /* @__PURE__ */ new Map();
411
- function loadDiskCache(projectRootDir) {
217
+ const stories = [];
412
218
  try {
413
- const cachePath = path4.join(projectRootDir, CACHE_FILE_NAME);
414
- if (fs2.existsSync(cachePath)) {
415
- const data = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
416
- for (const [key, value] of Object.entries(data)) {
417
- mtimeCache.set(key, value);
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
+ });
418
230
  }
419
- console.log(`[ASG] Loaded ${mtimeCache.size} entries from disk cache`);
420
231
  }
421
232
  } catch {
422
233
  }
423
- }
424
- function saveDiskCache(projectRootDir) {
425
- try {
426
- const cachePath = path4.join(projectRootDir, CACHE_FILE_NAME);
427
- const data = {};
428
- for (const [key, value] of mtimeCache) {
429
- data[key] = value;
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 {
430
240
  }
431
- fs2.writeFileSync(cachePath, JSON.stringify(data));
432
- } catch {
433
241
  }
434
- }
435
- function hasFileChanged(filePath) {
436
- try {
437
- const stat = fs2.statSync(filePath);
438
- const mtime = stat.mtimeMs;
439
- const cached = mtimeCache.get(filePath);
440
- if (cached === mtime) return false;
441
- mtimeCache.set(filePath, mtime);
442
- return true;
443
- } catch {
444
- mtimeCache.delete(filePath);
445
- return true;
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;
446
249
  }
447
- }
448
- function getStoryFilePath(filePath, storiesFolder) {
449
- const parsed = path4.parse(filePath);
450
- const storyName = `${parsed.name}.stories.tsx`;
451
- if (storiesFolder) {
452
- return path4.join(parsed.dir, storiesFolder, storyName);
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;
453
260
  }
454
- return path4.join(parsed.dir, storyName);
261
+ return moduleExports;
455
262
  }
456
- async function processBatch(files, options, projectRootDir, concurrency, generatedFiles) {
457
- let processed = 0;
458
- for (let i = 0; i < files.length; i += concurrency) {
459
- const chunk = files.slice(i, i + concurrency);
460
- await Promise.all(
461
- chunk.map(async (filePath) => {
462
- const result = await genStoryFile({
463
- options,
464
- id: filePath,
465
- projectRootDir
466
- });
467
- if (result) generatedFiles.push(result);
468
- processed++;
469
- })
470
- );
471
- }
472
- return processed;
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");
473
281
  }
474
- function createAutoStoryPlugin(options) {
475
- const projectRootDir = (options.projectRoot ?? process.cwd()).replace(/\\/g, "/");
476
- const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
477
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
478
- let hasRun = false;
479
- const allGeneratedFiles = [];
480
- console.log("[ASG] Plugin created", {
481
- preset: options.preset,
482
- imports: options.imports,
483
- isGenerateStoriesFileAtBuild: options.isGenerateStoriesFileAtBuild,
484
- storiesFolder: options.storiesFolder,
485
- projectRoot: projectRootDir,
486
- cwd: process.cwd()
487
- });
488
- async function generateAllStories() {
489
- if (hasRun) return;
490
- hasRun = true;
491
- if (!options.isGenerateStoriesFileAtBuild) {
492
- console.log("[ASG] Skipping \u2014 isGenerateStoriesFileAtBuild is false");
493
- return;
494
- }
495
- loadDiskCache(projectRootDir);
496
- const patterns = options.imports ?? ["src/**/*.tsx"];
497
- const ignorePatterns = options.ignores ?? [];
498
- console.log(`[ASG] Scanning for components: ${patterns.join(", ")}`);
499
- const allFiles = await getAllFilePaths({
500
- patterns,
501
- ignorePatterns,
502
- projectRootDir
503
- });
504
- const filesToProcess = allFiles.filter((filePath) => {
505
- if (filePath.includes(".stories")) return false;
506
- if (options.cacheEnabled !== false) {
507
- const storyPath = getStoryFilePath(filePath, options.storiesFolder);
508
- const storyExists = fs2.existsSync(storyPath);
509
- if (!hasFileChanged(filePath) && storyExists) return false;
510
- }
511
- return true;
512
- });
513
- console.log(
514
- `[ASG] Found ${allFiles.length} files, ${filesToProcess.length} need processing`
515
- );
516
- let totalProcessed = 0;
517
- for (let i = 0; i < filesToProcess.length; i += batchSize) {
518
- const batch = filesToProcess.slice(i, i + batchSize);
519
- const count = await processBatch(
520
- batch,
521
- options,
522
- projectRootDir,
523
- concurrency,
524
- allGeneratedFiles
525
- );
526
- totalProcessed += count;
527
- if (options.onProgress) {
528
- options.onProgress(totalProcessed, filesToProcess.length);
529
- }
530
- if (i + batchSize < filesToProcess.length) {
531
- await new Promise((r) => setTimeout(r, 0));
532
- }
533
- }
534
- saveDiskCache(projectRootDir);
535
- console.log(`[ASG] Generated stories for ${totalProcessed} components`);
536
- }
282
+ function moduleLoadCatchAllPlugin() {
283
+ let warnedDrift = false;
537
284
  return {
538
- name: PLUGIN_NAME,
539
- // configureServer fires for Vite dev server (Storybook's use case)
540
- // biome-ignore lint/suspicious/noExplicitAny: Vite server type varies across versions
541
- async configureServer(server) {
542
- console.log("[ASG] configureServer hook fired");
543
- const beforeCount = allGeneratedFiles.length;
544
- await generateAllStories();
545
- if (allGeneratedFiles.length > beforeCount && allGeneratedFiles.length > 0) {
546
- const firstFile = allGeneratedFiles[0];
547
- if (firstFile && server?.watcher?.emit) {
548
- console.log(
549
- `[ASG] Triggering Storybook rescan (${allGeneratedFiles.length} stories)`
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)."
550
300
  );
551
- server.watcher.emit("change", firstFile);
552
301
  }
302
+ return null;
553
303
  }
554
- },
555
- // buildStart fires for builds (fallback)
556
- async buildStart() {
557
- console.log("[ASG] buildStart hook fired");
558
- await generateAllStories();
559
- },
560
- async watchChange(id, change) {
561
- if (change.event === "delete") return;
562
- const normalizedId = id.replace(/\\/g, "/");
563
- if (options.imports) {
564
- const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
565
- const matches = options.imports.some(
566
- (pattern) => minimatch(relativePath, pattern)
567
- );
568
- if (!matches) return;
569
- }
570
- if (options.ignores) {
571
- const relativePath = normalizedId.replace(projectRootDir, "").replace(/^\//, "");
572
- const ignored = options.ignores.some(
573
- (pattern) => minimatch(relativePath, pattern)
574
- );
575
- if (ignored) return;
576
- }
577
- mtimeCache.delete(normalizedId);
578
- await genStoryFile({
579
- options,
580
- id: normalizedId,
581
- projectRootDir
582
- });
304
+ return { code: wrapped, map: null };
583
305
  }
584
306
  };
585
307
  }
586
- var index_default = { vite: createAutoStoryPlugin };
587
- var FIXED_MARKER = "// @onlook-fixed";
588
- var parser = withDefaultConfig({
589
- shouldExtractLiteralValuesFromEnum: true,
590
- shouldRemoveUndefinedFromOptional: true,
591
- propFilter: (prop) => {
592
- if (prop.parent?.fileName.includes("node_modules")) return false;
593
- return true;
594
- }
595
- });
596
- function resolveComponentPath(storyFilePath) {
597
- const dir = path4.dirname(storyFilePath);
598
- const parentDir = path4.dirname(dir);
599
- const storyName = path4.basename(storyFilePath);
600
- const componentName = storyName.replace(".stories.tsx", ".tsx");
601
- const componentPath = path4.join(parentDir, componentName);
602
- return fs2.existsSync(componentPath) ? componentPath : null;
603
- }
604
- function generateArgTypes(componentPath) {
605
- try {
606
- const docs = parser.parse(componentPath);
607
- if (docs.length === 0) return null;
608
- const doc = docs[0];
609
- if (!doc) return null;
610
- const argTypes = {};
611
- for (const [name, prop] of Object.entries(doc.props)) {
612
- if (prop.type.name.includes("=>") || prop.type.name === "Function") {
613
- continue;
614
- }
615
- const argType = {};
616
- if (prop.description) {
617
- argType.description = prop.description;
618
- }
619
- if (prop.type.name === "enum" && prop.type.value) {
620
- argType.control = { type: "select" };
621
- argType.options = prop.type.value.map(
622
- (v) => v.value
623
- );
624
- }
625
- if (prop.type.name === "boolean") {
626
- argType.control = { type: "boolean" };
627
- }
628
- if (prop.type.name === "number") {
629
- argType.control = { type: "number" };
630
- }
631
- if (Object.keys(argType).length > 0) {
632
- argTypes[name] = argType;
633
- }
634
- }
635
- return Object.keys(argTypes).length > 0 ? argTypes : null;
636
- } catch (err) {
637
- console.error(`[AutoStories] Failed to parse component: ${componentPath}`, err);
638
- return null;
639
- }
640
- }
641
- function enrichStoryFile(storyFilePath) {
642
- try {
643
- const content = fs2.readFileSync(storyFilePath, "utf-8");
644
- if (content.includes(FIXED_MARKER)) {
645
- return;
646
- }
647
- if (content.includes("argTypes:")) {
648
- return;
649
- }
650
- const componentPath = resolveComponentPath(storyFilePath);
651
- if (!componentPath) return;
652
- const argTypes = generateArgTypes(componentPath);
653
- if (!argTypes) return;
654
- const argTypesStr = JSON.stringify(argTypes, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
655
- const enriched = content.replace(
656
- /};\s*\nexport default meta;/,
657
- ` argTypes: ${argTypesStr},
658
- };
659
- export default meta;`
660
- );
661
- if (enriched !== content) {
662
- fs2.writeFileSync(storyFilePath, enriched);
663
- console.log(`[AutoStories] Enriched ${path4.basename(storyFilePath)} with argTypes`);
664
- }
665
- } catch (err) {
666
- console.error(`[AutoStories] Failed to enrich story: ${storyFilePath}`, err);
667
- }
668
- }
669
308
  function componentLocPlugin(options = {}) {
670
309
  const include = options.include ?? /\.(jsx|tsx)$/;
671
310
  const traverse = traverseModule.default ?? traverseModule;
@@ -682,13 +321,14 @@ function componentLocPlugin(options = {}) {
682
321
  const filepath = id.split("?", 1)[0];
683
322
  if (!filepath || filepath.includes("node_modules")) return null;
684
323
  if (!include.test(filepath)) return null;
324
+ if (/\.stories\.(jsx?|tsx?)$/.test(filepath)) return null;
685
325
  const ast = parse(code, {
686
326
  sourceType: "module",
687
327
  plugins: ["jsx", "typescript"],
688
328
  sourceFilename: filepath
689
329
  });
690
330
  let mutated = false;
691
- const relativePath = path4.relative(root, filepath);
331
+ const relativePath = path6.relative(root, filepath);
692
332
  traverse(ast, {
693
333
  JSXElement(nodePath) {
694
334
  const opening = nodePath.node.openingElement;
@@ -725,9 +365,31 @@ function componentLocPlugin(options = {}) {
725
365
  }
726
366
  };
727
367
  }
728
- var CACHE_DIR = path4.join(process.cwd(), ".storybook-cache");
729
- var SCREENSHOTS_DIR = path4.join(CACHE_DIR, "screenshots");
730
- var MANIFEST_PATH = path4.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");
731
393
  var VIEWPORT_WIDTH = 1920;
732
394
  var VIEWPORT_HEIGHT = 1080;
733
395
  var MIN_COMPONENT_WIDTH = 420;
@@ -735,30 +397,30 @@ var MIN_COMPONENT_HEIGHT = 280;
735
397
 
736
398
  // src/utils/fileSystem/fileSystem.ts
737
399
  function ensureCacheDirectories() {
738
- if (!fs2.existsSync(CACHE_DIR)) {
739
- fs2.mkdirSync(CACHE_DIR, { recursive: true });
400
+ if (!fs4.existsSync(CACHE_DIR)) {
401
+ fs4.mkdirSync(CACHE_DIR, { recursive: true });
740
402
  }
741
- if (!fs2.existsSync(SCREENSHOTS_DIR)) {
742
- fs2.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
403
+ if (!fs4.existsSync(SCREENSHOTS_DIR)) {
404
+ fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
743
405
  }
744
406
  }
745
407
  function computeFileHash(filePath) {
746
- if (!fs2.existsSync(filePath)) {
408
+ if (!fs4.existsSync(filePath)) {
747
409
  return "";
748
410
  }
749
- const content = fs2.readFileSync(filePath, "utf-8");
411
+ const content = fs4.readFileSync(filePath, "utf-8");
750
412
  return crypto.createHash("sha256").update(content).digest("hex");
751
413
  }
752
414
  function loadManifest() {
753
- if (fs2.existsSync(MANIFEST_PATH)) {
754
- const content = fs2.readFileSync(MANIFEST_PATH, "utf-8");
415
+ if (fs4.existsSync(MANIFEST_PATH)) {
416
+ const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
755
417
  return JSON.parse(content);
756
418
  }
757
419
  return { stories: {} };
758
420
  }
759
421
  function saveManifest(manifest) {
760
422
  ensureCacheDirectories();
761
- fs2.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
423
+ fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
762
424
  }
763
425
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
764
426
  const manifest = loadManifest();
@@ -774,22 +436,82 @@ function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
774
436
  };
775
437
  saveManifest(manifest);
776
438
  }
777
- 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
+ async function installChromium() {
449
+ if (!installPromise) {
450
+ installPromise = (async () => {
451
+ const cliPath = require2.resolve("playwright-core/cli.js");
452
+ console.log(
453
+ `[STORYBOOK_PLUGIN] chromium binary missing \u2014 installing via ${cliPath}`
454
+ );
455
+ try {
456
+ const { stdout, stderr } = await execFileAsync(
457
+ process.execPath,
458
+ [cliPath, "install", "--force", "chromium"],
459
+ { timeout: 5 * 60 * 1e3, maxBuffer: 32 * 1024 * 1024 }
460
+ );
461
+ if (stdout)
462
+ console.log(`[STORYBOOK_PLUGIN] playwright install: ${stdout.slice(-1e3)}`);
463
+ if (stderr)
464
+ console.log(
465
+ `[STORYBOOK_PLUGIN] playwright install stderr: ${stderr.slice(-1e3)}`
466
+ );
467
+ console.log("[STORYBOOK_PLUGIN] chromium installed");
468
+ } catch (err) {
469
+ const detail = err instanceof Error ? err.message : String(err);
470
+ console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
471
+ throw new Error(`Playwright chromium install failed: ${detail}`);
472
+ } finally {
473
+ installPromise = null;
474
+ }
475
+ })();
476
+ }
477
+ return installPromise;
478
+ }
479
+ async function launchWithSelfHeal() {
480
+ try {
481
+ return await chromium.launch({ headless: true });
482
+ } catch (err) {
483
+ if (!isMissingExecutableError(err)) throw err;
484
+ console.log("[STORYBOOK_PLUGIN] launch failed, attempting self-heal install");
485
+ await installChromium();
486
+ try {
487
+ return await chromium.launch({ headless: true });
488
+ } catch (postInstallErr) {
489
+ const detail = postInstallErr instanceof Error ? postInstallErr.message : String(postInstallErr);
490
+ console.error(
491
+ `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
492
+ );
493
+ throw new Error(
494
+ `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
495
+ );
496
+ }
497
+ }
498
+ }
778
499
  async function getBrowser() {
779
- if (!browser) {
780
- browser = await chromium.launch({
781
- headless: true
500
+ if (!browserPromise) {
501
+ browserPromise = launchWithSelfHeal().catch((err) => {
502
+ browserPromise = null;
503
+ throw err;
782
504
  });
783
505
  }
784
- return browser;
506
+ return browserPromise;
785
507
  }
786
508
  function getScreenshotPath(storyId, theme) {
787
- const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
788
- return path4.join(storyDir, `${theme}.png`);
509
+ const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
510
+ return path6.join(storyDir, `${theme}.png`);
789
511
  }
790
512
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
791
- const browser2 = await getBrowser();
792
- const context = await browser2.newContext({
513
+ const browser = await getBrowser();
514
+ const context = await browser.newContext({
793
515
  viewport: { width, height },
794
516
  deviceScaleFactor: 2
795
517
  });
@@ -872,9 +594,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
872
594
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
873
595
  try {
874
596
  ensureCacheDirectories();
875
- const storyDir = path4.join(SCREENSHOTS_DIR, storyId);
876
- if (!fs2.existsSync(storyDir)) {
877
- fs2.mkdirSync(storyDir, { recursive: true });
597
+ const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
598
+ if (!fs4.existsSync(storyDir)) {
599
+ fs4.mkdirSync(storyDir, { recursive: true });
878
600
  }
879
601
  const screenshotPath = getScreenshotPath(storyId, theme);
880
602
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -885,7 +607,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
885
607
  storybookUrl,
886
608
  timeoutMs
887
609
  );
888
- fs2.writeFileSync(screenshotPath, buffer);
610
+ fs4.writeFileSync(screenshotPath, buffer);
889
611
  return { path: screenshotPath, boundingBox };
890
612
  } catch (error) {
891
613
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -893,12 +615,93 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
893
615
  }
894
616
  }
895
617
 
618
+ // src/screenshot-service/screenshot-service.ts
619
+ process.env.ONBOOK_PROXY_TOKEN ?? null;
620
+ process.env.ONBOOK_BROADCAST_URL ?? null;
621
+
622
+ // src/utils/notifyOnbookIndexChanged/notifyOnbookIndexChanged.ts
623
+ var DEBOUNCE_MS = 300;
624
+ var REQUEST_TIMEOUT_MS = 5e3;
625
+ var pending = null;
626
+ var inFlight = false;
627
+ var pendingTrailingCall = false;
628
+ function notifyOnbookIndexChanged() {
629
+ const proxyToken = process.env.ONBOOK_PROXY_TOKEN;
630
+ const broadcastUrl = process.env.ONBOOK_BROADCAST_URL;
631
+ if (!proxyToken || !broadcastUrl) return;
632
+ if (pending) clearTimeout(pending);
633
+ pending = setTimeout(() => {
634
+ pending = null;
635
+ if (inFlight) {
636
+ pendingTrailingCall = true;
637
+ return;
638
+ }
639
+ void fire(proxyToken, broadcastUrl);
640
+ }, DEBOUNCE_MS);
641
+ }
642
+ async function fire(proxyToken, broadcastUrl) {
643
+ inFlight = true;
644
+ try {
645
+ const res = await fetch(broadcastUrl, {
646
+ method: "POST",
647
+ headers: {
648
+ Authorization: `Bearer ${proxyToken}`,
649
+ "Content-Type": "application/json"
650
+ },
651
+ body: JSON.stringify({ event: { type: "STORYBOOK_INDEX_CHANGED" } }),
652
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
653
+ });
654
+ if (!res.ok) {
655
+ console.warn("[STORYBOOK_PLUGIN] broadcast non-2xx", {
656
+ status: res.status,
657
+ statusText: res.statusText
658
+ });
659
+ }
660
+ } catch (err) {
661
+ console.warn(
662
+ "[STORYBOOK_PLUGIN] index-changed broadcast failed",
663
+ err instanceof Error ? err.message : String(err)
664
+ );
665
+ } finally {
666
+ inFlight = false;
667
+ if (pendingTrailingCall) {
668
+ pendingTrailingCall = false;
669
+ notifyOnbookIndexChanged();
670
+ }
671
+ }
672
+ }
673
+
674
+ // src/utils/notifyOnbookIndexChanged/waitForIndexAndBroadcast.ts
675
+ var PROBE_INTERVAL_MS = 500;
676
+ var PROBE_CEILING_MS = 5 * 60 * 1e3;
677
+ async function waitForIndexAndBroadcast(port = 6006) {
678
+ const url = `http://localhost:${port}/onbook-index.json`;
679
+ const deadline = Date.now() + PROBE_CEILING_MS;
680
+ while (Date.now() < deadline) {
681
+ try {
682
+ const res = await fetch(url, {
683
+ signal: AbortSignal.timeout(2e3)
684
+ });
685
+ if (res.ok) {
686
+ notifyOnbookIndexChanged();
687
+ return;
688
+ }
689
+ } catch {
690
+ }
691
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
692
+ }
693
+ console.warn("[STORYBOOK_PLUGIN] waitForIndexAndBroadcast hit ceiling without a 2xx", {
694
+ url,
695
+ ceilingMs: PROBE_CEILING_MS
696
+ });
697
+ }
698
+
896
699
  // src/handlers/handleStoryFileChange/handleStoryFileChange.ts
897
700
  var cachedIndex = null;
898
701
  var indexFetchPromise = null;
899
702
  var pendingFiles = /* @__PURE__ */ new Set();
900
703
  var debounceTimer = null;
901
- var DEBOUNCE_MS = 500;
704
+ var DEBOUNCE_MS2 = 500;
902
705
  async function fetchStorybookIndex() {
903
706
  try {
904
707
  const response = await fetch("http://localhost:6006/index.json");
@@ -912,7 +715,7 @@ async function fetchStorybookIndex() {
912
715
  }
913
716
  function getStoriesForFile(filePath) {
914
717
  if (!cachedIndex) return [];
915
- const fileName = path4.basename(filePath);
718
+ const fileName = path6.basename(filePath);
916
719
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
917
720
  }
918
721
  async function regenerateScreenshotsForFiles(files) {
@@ -957,6 +760,7 @@ async function regenerateScreenshotsForFiles(files) {
957
760
  function handleStoryFileChange({ file, modules }) {
958
761
  if (file.endsWith(".stories.tsx") || file.endsWith(".stories.ts")) {
959
762
  console.log(`[Screenshots] Story file changed: ${file}`);
763
+ notifyOnbookIndexChanged();
960
764
  if (!cachedIndex && !indexFetchPromise) {
961
765
  indexFetchPromise = fetchStorybookIndex();
962
766
  }
@@ -973,10 +777,123 @@ function handleStoryFileChange({ file, modules }) {
973
777
  } catch (error) {
974
778
  console.error("[Screenshots] Error regenerating screenshots:", error);
975
779
  }
976
- }, DEBOUNCE_MS);
780
+ }, DEBOUNCE_MS2);
977
781
  return modules;
978
782
  }
979
783
  }
784
+
785
+ // src/screenshot-service/utils/console-logs/console-logs.ts
786
+ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
787
+ const browser = await getBrowser();
788
+ const context = await browser.newContext({
789
+ viewport: { width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT }
790
+ });
791
+ const page = await context.newPage();
792
+ const logs = [];
793
+ const pageErrors = [];
794
+ page.on("console", (msg) => {
795
+ const level = msg.type();
796
+ logs.push({
797
+ level: level === "warning" ? "warn" : level,
798
+ text: msg.text(),
799
+ timestamp: Date.now()
800
+ });
801
+ });
802
+ page.on("pageerror", (err) => {
803
+ pageErrors.push(err.message);
804
+ });
805
+ const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
806
+ try {
807
+ await page.goto(url, { timeout: timeoutMs });
808
+ await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
809
+ await page.waitForLoadState("load", { timeout: timeoutMs });
810
+ try {
811
+ await page.waitForLoadState("networkidle", { timeout: 5e3 });
812
+ } catch {
813
+ }
814
+ await new Promise((r) => setTimeout(r, 1e3));
815
+ return { logs, pageErrors, url };
816
+ } finally {
817
+ await context.close();
818
+ }
819
+ }
820
+
821
+ // src/soft-story-rerender/soft-story-rerender.ts
822
+ function softStoryRerenderPlugin() {
823
+ let warnedOnce = false;
824
+ return {
825
+ name: "onbook-soft-story-rerender",
826
+ enforce: "post",
827
+ apply: "serve",
828
+ transform(code, _id) {
829
+ if (!code.includes("__STORYBOOK_PREVIEW__.onStoriesChanged")) return null;
830
+ 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*\}\);/;
831
+ if (!pattern.test(code)) {
832
+ if (!warnedOnce) {
833
+ warnedOnce = true;
834
+ console.warn(
835
+ "[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."
836
+ );
837
+ }
838
+ return null;
839
+ }
840
+ const replacement = `import.meta.hot.accept($QUOTE$$PATH$$QUOTE$, async (newModule) => {
841
+ // ONL-1176 soft rerender \u2014 call rerender() on each active StoryRender
842
+ // instead of preview.onStoriesChanged() (which tears down + remounts).
843
+ // This accept handler fires in EVERY canvas iframe whenever ANY story
844
+ // file changes (every iframe's preview bundle imports the same virtual
845
+ // stories index). We identity-check each render's unboundStoryFn so
846
+ // iframes whose underlying story file didn't change stay perfectly
847
+ // still \u2014 no 1-frame no-op tick.
848
+ const preview = window.__STORYBOOK_PREVIEW__;
849
+ if (!preview || !preview.storyStoreValue) {
850
+ window.__STORYBOOK_PREVIEW__?.onStoriesChanged({ importFn: newModule.importFn });
851
+ return;
852
+ }
853
+ try {
854
+ await preview.storyStoreValue.onStoriesChanged({ importFn: newModule.importFn });
855
+ const renders = preview.storyRenders || [];
856
+ if (renders.length === 0) {
857
+ preview.onStoriesChanged({ importFn: newModule.importFn });
858
+ return;
859
+ }
860
+ await Promise.all(
861
+ renders.map(async (r) => {
862
+ try {
863
+ const nextStory = await preview.storyStoreValue.loadStory({ storyId: r.id });
864
+ if (!nextStory) return;
865
+ const prev = r.story;
866
+ const unchanged =
867
+ prev &&
868
+ prev.unboundStoryFn === nextStory.unboundStoryFn &&
869
+ prev.storyFn === nextStory.storyFn &&
870
+ prev.moduleExport === nextStory.moduleExport &&
871
+ prev.playFunction === nextStory.playFunction;
872
+ if (unchanged) return;
873
+ r.story = nextStory;
874
+ // Stories using the \`mount\` API can't be rerendered in place
875
+ // (their play function owns mounting), so fall back to remount.
876
+ if (nextStory.usesMount && r.remount) {
877
+ return r.remount();
878
+ }
879
+ return r.rerender();
880
+ } catch (innerErr) {
881
+ console.warn('[onbook] rerender failed for story', r.id, innerErr);
882
+ }
883
+ }),
884
+ );
885
+ } catch (err) {
886
+ console.warn('[onbook] soft rerender failed, falling back', err);
887
+ preview.onStoriesChanged({ importFn: newModule.importFn });
888
+ }
889
+ });`;
890
+ const out = code.replace(pattern, (_match, quote, path7) => {
891
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path7}${quote}`);
892
+ });
893
+ return out === code ? null : { code: out, map: null };
894
+ }
895
+ };
896
+ }
980
897
  function findGitRoot(startPath) {
981
898
  let currentPath = startPath;
982
899
  while (currentPath !== dirname(currentPath)) {
@@ -995,86 +912,71 @@ var gitRoot = findGitRoot(storybookDir);
995
912
  var storybookLocation = gitRoot ? relative(gitRoot, storybookDir) : "";
996
913
  var repoRoot = gitRoot || process.cwd();
997
914
  var DEFAULT_ALLOWED_ORIGINS = [
998
- "https://app.onlook.ai",
915
+ "https://app.onlook.com",
999
916
  "http://localhost:3000",
1000
917
  "http://localhost:6006"
1001
918
  ];
1002
- var AUTO_STORIES_FOLDER = ".onlook-stories";
1003
- var storyRuntimeErrors = /* @__PURE__ */ new Map();
1004
- var serveMetadataAndScreenshots = (req, res, next) => {
1005
- if (req.url === "/onbook-health.json") {
1006
- const cacheBuster = Date.now();
1007
- fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
1008
- cache: "no-store"
1009
- }).then((response) => response.json()).then((indexData) => {
1010
- const entries = indexData.entries || {};
1011
- const autoStories = [];
1012
- const userStories = [];
1013
- for (const [storyId, entry] of Object.entries(entries)) {
1014
- const importPath = entry.importPath || "";
1015
- if (importPath.includes(AUTO_STORIES_FOLDER)) {
1016
- autoStories.push(storyId);
1017
- } else {
1018
- userStories.push(storyId);
1019
- }
1020
- }
1021
- const healthy = autoStories.filter((id) => !storyRuntimeErrors.has(id));
1022
- const broken = autoStories.filter((id) => storyRuntimeErrors.has(id)).map((id) => ({
1023
- storyId: id,
1024
- // biome-ignore lint/style/noNonNullAssertion: filtered above
1025
- error: storyRuntimeErrors.get(id)
1026
- }));
1027
- res.setHeader("Content-Type", "application/json");
1028
- res.setHeader("Access-Control-Allow-Origin", "*");
1029
- res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1030
- res.end(
1031
- JSON.stringify({
1032
- autoStoryCount: autoStories.length,
1033
- userStoryCount: userStories.length,
1034
- healthy,
1035
- broken
1036
- })
1037
- );
1038
- }).catch((error) => {
1039
- res.statusCode = 500;
1040
- res.setHeader("Content-Type", "application/json");
1041
- res.end(
1042
- JSON.stringify({
1043
- error: "Failed to fetch story index",
1044
- details: String(error)
1045
- })
1046
- );
919
+ var manifestCache = null;
920
+ var manifestHash = null;
921
+ function readManifestFromDisk(filePath) {
922
+ try {
923
+ if (!fs4.existsSync(filePath)) return null;
924
+ const raw = fs4.readFileSync(filePath, "utf-8");
925
+ const hash = createHash("sha1").update(raw).digest("hex");
926
+ const manifest = JSON.parse(raw);
927
+ return { manifest, hash };
928
+ } catch (err) {
929
+ console.warn("[STORYBOOK_PLUGIN] Failed to read manifest", {
930
+ filePath,
931
+ error: err instanceof Error ? err.message : String(err)
1047
932
  });
1048
- return;
933
+ return null;
1049
934
  }
1050
- if (req.url === "/onbook-report-error" && req.method === "POST") {
1051
- let body = "";
1052
- req.on("data", (chunk) => {
1053
- body += chunk.toString();
1054
- });
1055
- req.on("end", () => {
1056
- try {
1057
- const { storyId, error } = JSON.parse(body);
1058
- if (storyId && error) {
1059
- storyRuntimeErrors.set(storyId, error);
1060
- console.log(
1061
- `[STORYBOOK_PLUGIN] Story runtime error reported: ${storyId}`,
1062
- error
1063
- );
1064
- }
1065
- res.setHeader("Access-Control-Allow-Origin", "*");
1066
- res.statusCode = 200;
1067
- res.end("ok");
1068
- } catch {
1069
- res.statusCode = 400;
1070
- res.end("Invalid JSON");
1071
- }
1072
- });
1073
- return;
935
+ }
936
+ function refreshManifest(filePath) {
937
+ const result = readManifestFromDisk(filePath);
938
+ if (!result) {
939
+ return false;
1074
940
  }
941
+ if (result.hash === manifestHash) return false;
942
+ manifestCache = result.manifest;
943
+ manifestHash = result.hash;
944
+ console.log("[STORYBOOK_PLUGIN] Manifest cache refreshed", {
945
+ filePath,
946
+ hash: result.hash.slice(0, 8),
947
+ storyCount: Object.keys(result.manifest.stories ?? {}).length
948
+ });
949
+ return true;
950
+ }
951
+ var TAILWIND_ENTRY_CANDIDATES = [
952
+ "src/app/globals.css",
953
+ "app/globals.css",
954
+ "src/index.css",
955
+ "src/main.css",
956
+ "src/styles.css",
957
+ "src/styles/globals.css",
958
+ "src/styles/index.css",
959
+ "styles/globals.css",
960
+ "styles/index.css",
961
+ "app.css",
962
+ "index.css"
963
+ ];
964
+ var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
965
+ function detectTailwindEntryCss(cwd) {
966
+ for (const rel of TAILWIND_ENTRY_CANDIDATES) {
967
+ const abs = path6.join(cwd, rel);
968
+ try {
969
+ if (!fs4.existsSync(abs)) continue;
970
+ const head = fs4.readFileSync(abs, "utf-8").slice(0, 2048);
971
+ if (TAILWIND_DIRECTIVE_PATTERN.test(head)) return abs;
972
+ } catch {
973
+ }
974
+ }
975
+ return null;
976
+ }
977
+ var serveMetadataAndScreenshots = (req, res, next) => {
1075
978
  if (req.url === "/onbook-index.json") {
1076
979
  console.log("[STORYBOOK_PLUGIN] Serving /onbook-index.json endpoint");
1077
- const manifestPath = path4.join(process.cwd(), ".storybook-cache", "manifest.json");
1078
980
  const cacheBuster = Date.now();
1079
981
  console.log("[STORYBOOK_PLUGIN] Fetching http://localhost:6006/index.json");
1080
982
  fetch(`http://localhost:6006/index.json?_t=${cacheBuster}`, {
@@ -1089,20 +991,9 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1089
991
  ok: response.ok,
1090
992
  statusText: response.statusText
1091
993
  });
1092
- if (!response.ok) {
1093
- console.log("[STORYBOOK_PLUGIN] Index not ready yet", {
1094
- status: response.status
1095
- });
1096
- res.statusCode = 503;
1097
- res.setHeader("Content-Type", "application/json");
1098
- res.setHeader("Retry-After", "10");
1099
- res.end(JSON.stringify({ code: "INDEX_NOT_READY" }));
1100
- return null;
1101
- }
1102
994
  return response.json();
1103
995
  }).then((indexData) => {
1104
- if (!indexData) return;
1105
- const manifest = fs2.existsSync(manifestPath) ? JSON.parse(fs2.readFileSync(manifestPath, "utf-8")) : { stories: {} };
996
+ const manifest = manifestCache ?? { stories: {} };
1106
997
  const defaultBoundingBox = { width: 1920, height: 1080 };
1107
998
  for (const [storyId, entry] of Object.entries(indexData.entries || {})) {
1108
999
  const manifestEntry = manifest.stories?.[storyId];
@@ -1120,17 +1011,15 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1120
1011
  res.setHeader("Expires", "0");
1121
1012
  res.end(JSON.stringify(indexData));
1122
1013
  }).catch((error) => {
1123
- console.log("[STORYBOOK_PLUGIN] Index not available", {
1124
- error: error instanceof Error ? error.message : String(error)
1014
+ console.error("[STORYBOOK_PLUGIN] Failed to fetch/extend index.json", {
1015
+ error: error instanceof Error ? error.message : String(error),
1016
+ errorType: error instanceof Error ? error.constructor.name : typeof error,
1017
+ stack: error instanceof Error ? error.stack : void 0
1125
1018
  });
1126
- res.statusCode = 503;
1019
+ res.statusCode = 500;
1127
1020
  res.setHeader("Content-Type", "application/json");
1128
- res.setHeader("Retry-After", "10");
1129
1021
  res.end(
1130
- JSON.stringify({
1131
- code: "INDEX_NOT_READY",
1132
- details: String(error)
1133
- })
1022
+ JSON.stringify({ error: "Failed to fetch index", details: String(error) })
1134
1023
  );
1135
1024
  });
1136
1025
  return;
@@ -1171,8 +1060,42 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1171
1060
  });
1172
1061
  return;
1173
1062
  }
1063
+ if (req.url?.startsWith("/api/console-logs")) {
1064
+ const url = new URL(req.url, "http://localhost");
1065
+ const storyId = url.searchParams.get("storyId");
1066
+ const theme = url.searchParams.get("theme") || "light";
1067
+ if (!storyId) {
1068
+ res.statusCode = 400;
1069
+ res.setHeader("Content-Type", "application/json");
1070
+ res.end(JSON.stringify({ error: "storyId is required" }));
1071
+ return;
1072
+ }
1073
+ if (theme !== "light" && theme !== "dark") {
1074
+ res.statusCode = 400;
1075
+ res.setHeader("Content-Type", "application/json");
1076
+ res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1077
+ return;
1078
+ }
1079
+ captureConsoleLogs(storyId, theme).then((result) => {
1080
+ res.setHeader("Content-Type", "application/json");
1081
+ res.setHeader("Access-Control-Allow-Origin", "*");
1082
+ res.setHeader("Cache-Control", "no-cache");
1083
+ res.end(JSON.stringify(result));
1084
+ }).catch((error) => {
1085
+ console.error("Console log capture error:", error);
1086
+ res.statusCode = 500;
1087
+ res.setHeader("Content-Type", "application/json");
1088
+ res.end(
1089
+ JSON.stringify({
1090
+ error: "Failed to capture console logs",
1091
+ details: String(error)
1092
+ })
1093
+ );
1094
+ });
1095
+ return;
1096
+ }
1174
1097
  if (req.url?.startsWith("/screenshots/")) {
1175
- const screenshotPath = path4.join(
1098
+ const screenshotPath = path6.join(
1176
1099
  process.cwd(),
1177
1100
  ".storybook-cache",
1178
1101
  req.url.replace("/screenshots/", "screenshots/")
@@ -1181,11 +1104,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1181
1104
  const storyId = urlParts[0];
1182
1105
  const themeFile = urlParts[1];
1183
1106
  const theme = themeFile?.replace(".png", "");
1184
- if (fs2.existsSync(screenshotPath)) {
1107
+ if (fs4.existsSync(screenshotPath)) {
1185
1108
  res.setHeader("Content-Type", "image/png");
1186
1109
  res.setHeader("Access-Control-Allow-Origin", "*");
1187
1110
  res.setHeader("Cache-Control", "public, max-age=3600");
1188
- fs2.createReadStream(screenshotPath).pipe(res);
1111
+ fs4.createReadStream(screenshotPath).pipe(res);
1189
1112
  return;
1190
1113
  }
1191
1114
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -1193,16 +1116,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1193
1116
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1194
1117
  );
1195
1118
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1196
- const storyDir = path4.join(
1119
+ const storyDir = path6.join(
1197
1120
  process.cwd(),
1198
1121
  ".storybook-cache",
1199
1122
  "screenshots",
1200
1123
  storyId
1201
1124
  );
1202
- if (!fs2.existsSync(storyDir)) {
1203
- fs2.mkdirSync(storyDir, { recursive: true });
1125
+ if (!fs4.existsSync(storyDir)) {
1126
+ fs4.mkdirSync(storyDir, { recursive: true });
1204
1127
  }
1205
- fs2.writeFileSync(screenshotPath, buffer);
1128
+ fs4.writeFileSync(screenshotPath, buffer);
1206
1129
  res.setHeader("Content-Type", "image/png");
1207
1130
  res.setHeader("Access-Control-Allow-Origin", "*");
1208
1131
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -1214,6 +1137,8 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1214
1137
  );
1215
1138
  res.statusCode = 500;
1216
1139
  res.setHeader("Content-Type", "application/json");
1140
+ res.setHeader("Access-Control-Allow-Origin", "*");
1141
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1217
1142
  res.end(
1218
1143
  JSON.stringify({
1219
1144
  error: "Failed to generate screenshot",
@@ -1224,11 +1149,41 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1224
1149
  return;
1225
1150
  }
1226
1151
  res.statusCode = 404;
1152
+ res.setHeader("Access-Control-Allow-Origin", "*");
1153
+ res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
1227
1154
  res.end("Screenshot not found");
1228
1155
  return;
1229
1156
  }
1230
1157
  next();
1231
1158
  };
1159
+ var ONLOOK_PLUGIN_CLAIMS_GLOBAL = "__ONLOOK_STORYBOOK_PLUGIN_CLAIMS__";
1160
+ function claimRegistry() {
1161
+ const g = globalThis;
1162
+ let registry = g[ONLOOK_PLUGIN_CLAIMS_GLOBAL];
1163
+ if (!registry) {
1164
+ registry = /* @__PURE__ */ new WeakMap();
1165
+ g[ONLOOK_PLUGIN_CLAIMS_GLOBAL] = registry;
1166
+ }
1167
+ return registry;
1168
+ }
1169
+ function withDedupeApply(plugins, instanceId) {
1170
+ for (const plugin of plugins) {
1171
+ const prior = plugin.apply;
1172
+ plugin.apply = (config, env) => {
1173
+ const priorOk = prior === void 0 ? true : typeof prior === "string" ? env.command === prior : prior(config, env);
1174
+ if (!priorOk) return false;
1175
+ if (env === null || typeof env !== "object") return true;
1176
+ const registry = claimRegistry();
1177
+ const claimed = registry.get(env);
1178
+ if (claimed === void 0) {
1179
+ registry.set(env, instanceId);
1180
+ return true;
1181
+ }
1182
+ return claimed === instanceId;
1183
+ };
1184
+ }
1185
+ return plugins;
1186
+ }
1232
1187
  function storybookOnlookPlugin(options = {}) {
1233
1188
  if (process.env.CHROMATIC || process.env.CI) {
1234
1189
  console.log("[STORYBOOK_PLUGIN] Disabled in CI/Chromatic environment");
@@ -1236,25 +1191,28 @@ function storybookOnlookPlugin(options = {}) {
1236
1191
  }
1237
1192
  const port = options.port ?? 6006;
1238
1193
  const allowedOrigins = [...DEFAULT_ALLOWED_ORIGINS, ...options.allowedOrigins ?? []];
1194
+ const hmrOverride = options.hmr;
1239
1195
  console.log("[STORYBOOK_PLUGIN] Plugin initialized", {
1240
1196
  port,
1241
1197
  allowedOrigins,
1242
1198
  storybookLocation,
1243
- repoRoot
1199
+ repoRoot,
1200
+ hmr: hmrOverride ?? null
1244
1201
  });
1245
1202
  const mainPlugin = {
1246
1203
  name: "storybook-onlook-plugin",
1247
1204
  config() {
1248
1205
  return {
1249
1206
  server: {
1250
- // E2B sandbox HMR configuration
1251
- hmr: {
1252
- // E2B sandboxes use HTTPS, so we need secure WebSocket
1253
- protocol: "wss",
1254
- // E2B routes through standard HTTPS port 443
1255
- clientPort: 443,
1256
- // The actual Storybook server port inside the sandbox
1257
- port
1207
+ // HMR override applies only when the caller explicitly opts in
1208
+ // (typically: Storybook fronted over HTTPS by a reverse proxy).
1209
+ // Locally, omitting this lets Vite use defaults
1210
+ // (ws://localhost:<port>) and HMR works as expected.
1211
+ ...hmrOverride && {
1212
+ hmr: {
1213
+ ...hmrOverride,
1214
+ port
1215
+ }
1258
1216
  },
1259
1217
  cors: {
1260
1218
  origin: allowedOrigins
@@ -1265,67 +1223,112 @@ function storybookOnlookPlugin(options = {}) {
1265
1223
  configureServer(server) {
1266
1224
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1267
1225
  server.middlewares.use(serveMetadataAndScreenshots);
1226
+ const cachedManifestPath = path6.join(
1227
+ process.cwd(),
1228
+ ".storybook-cache",
1229
+ "manifest.json"
1230
+ );
1231
+ refreshManifest(cachedManifestPath);
1232
+ let pending2;
1233
+ const scheduleRefresh = (file) => {
1234
+ if (file !== cachedManifestPath) return;
1235
+ if (pending2) clearTimeout(pending2);
1236
+ pending2 = setTimeout(() => {
1237
+ pending2 = void 0;
1238
+ refreshManifest(cachedManifestPath);
1239
+ }, 100);
1240
+ };
1241
+ server.watcher.on("add", scheduleRefresh);
1242
+ server.watcher.on("change", scheduleRefresh);
1243
+ const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1244
+ const resolvedStoryGlobs = storyGlobs.map(
1245
+ (glob) => path6.isAbsolute(glob) ? glob : path6.join(process.cwd(), glob)
1246
+ );
1247
+ if (resolvedStoryGlobs.length > 0) {
1248
+ server.watcher.add(resolvedStoryGlobs);
1249
+ console.log("[STORYBOOK_PLUGIN] Extended watcher coverage", {
1250
+ storyGlobs: resolvedStoryGlobs
1251
+ });
1252
+ }
1253
+ const tailwindEntryPath = (() => {
1254
+ if (options.tailwindEntryCss === false) return null;
1255
+ if (typeof options.tailwindEntryCss === "string") {
1256
+ return path6.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path6.join(process.cwd(), options.tailwindEntryCss);
1257
+ }
1258
+ return detectTailwindEntryCss(process.cwd());
1259
+ })();
1260
+ if (tailwindEntryPath) {
1261
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan watcher enabled", {
1262
+ entry: tailwindEntryPath,
1263
+ autoDetected: options.tailwindEntryCss === void 0
1264
+ });
1265
+ const seenDirs = /* @__PURE__ */ new Set();
1266
+ let watcherReady = false;
1267
+ server.watcher.once("ready", () => {
1268
+ watcherReady = true;
1269
+ });
1270
+ server.watcher.on("add", (file) => {
1271
+ if (!/\.tsx?$/.test(file)) return;
1272
+ const dir = path6.dirname(file);
1273
+ const isNewDir = !seenDirs.has(dir);
1274
+ seenDirs.add(dir);
1275
+ if (!watcherReady || !isNewDir) return;
1276
+ try {
1277
+ const now = /* @__PURE__ */ new Date();
1278
+ fs4.utimesSync(tailwindEntryPath, now, now);
1279
+ console.log("[STORYBOOK_PLUGIN] Tailwind rescan for new dir", {
1280
+ dir,
1281
+ entry: tailwindEntryPath
1282
+ });
1283
+ } catch (err) {
1284
+ console.warn("[STORYBOOK_PLUGIN] Tailwind rescan: utimes failed", {
1285
+ entry: tailwindEntryPath,
1286
+ error: err instanceof Error ? err.message : String(err)
1287
+ });
1288
+ }
1289
+ });
1290
+ }
1268
1291
  server.httpServer?.once("listening", () => {
1292
+ const probePort = server.config.server.port ?? port;
1269
1293
  console.log("[STORYBOOK_PLUGIN] Server is listening", {
1270
- port: server.config.server.port,
1294
+ port: probePort,
1271
1295
  host: server.config.server.host
1272
1296
  });
1297
+ void waitForIndexAndBroadcast(probePort);
1273
1298
  });
1274
1299
  },
1275
1300
  configurePreviewServer(server) {
1276
1301
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1277
1302
  server.middlewares.use(serveMetadataAndScreenshots);
1303
+ refreshManifest(path6.join(process.cwd(), ".storybook-cache", "manifest.json"));
1278
1304
  },
1279
- handleHotUpdate(ctx) {
1280
- if (ctx.file.includes(AUTO_STORIES_FOLDER) && ctx.file.endsWith(".stories.tsx")) {
1281
- enrichStoryFile(ctx.file);
1282
- }
1283
- return handleStoryFileChange(ctx);
1284
- }
1305
+ handleHotUpdate: handleStoryFileChange
1285
1306
  };
1286
- const plugins = [componentLocPlugin(), mainPlugin];
1287
- if (options.autoStories !== false) {
1288
- const imports = options.autoStories ?? ["src/**/*.tsx"];
1289
- const ignores = options.autoStoriesIgnore ?? [
1290
- "src/**/*.stories.tsx",
1291
- "src/**/*.stories.ts",
1292
- "src/**/*.test.tsx",
1293
- "src/**/*.test.ts",
1294
- "src/**/*.spec.tsx",
1295
- "src/**/*.spec.ts",
1296
- "node_modules/**",
1297
- "**/.onlook-stories/**",
1298
- // Next.js route files (not renderable as Storybook stories)
1299
- "src/**/page.tsx",
1300
- "src/**/layout.tsx",
1301
- "src/**/loading.tsx",
1302
- "src/**/error.tsx",
1303
- "src/**/not-found.tsx",
1304
- "src/**/template.tsx"
1305
- ];
1306
- console.log("[STORYBOOK_PLUGIN] Auto-story generation enabled", {
1307
- imports,
1308
- ignores,
1309
- storiesFolder: AUTO_STORIES_FOLDER
1310
- });
1307
+ const envContainment = envContainmentPlugin(options.envContainment);
1308
+ const moduleLoadCatchAll = options.moduleLoadCatchAll === false ? null : moduleLoadCatchAllPlugin();
1309
+ return withDedupeApply(
1310
+ [
1311
+ ...envContainment ? [envContainment] : [],
1312
+ ...moduleLoadCatchAll ? [moduleLoadCatchAll] : [],
1313
+ componentLocPlugin(),
1314
+ mainPlugin,
1315
+ fastRefreshTolerantExportsPlugin(),
1316
+ softStoryRerenderPlugin()
1317
+ ],
1318
+ /* @__PURE__ */ Symbol("onlook-storybook-plugin-instance")
1319
+ );
1320
+ }
1321
+ var tolerantCsfIndexer = {
1322
+ test: /(stories|story)\.(m?js|ts)x?$/,
1323
+ createIndex: async (fileName, options) => {
1311
1324
  try {
1312
- plugins.push(
1313
- index_default.vite({
1314
- preset: "react",
1315
- imports,
1316
- ignores,
1317
- storiesFolder: AUTO_STORIES_FOLDER,
1318
- isGenerateStoriesFileAtBuild: true
1319
- })
1320
- );
1325
+ return (await readCsf(fileName, options)).parse().indexInputs;
1321
1326
  } catch (err) {
1322
- console.error(
1323
- "[STORYBOOK_PLUGIN] ASG plugin failed to initialize, continuing without auto-stories",
1324
- err
1325
- );
1327
+ const msg = err instanceof Error ? err.message : String(err);
1328
+ console.warn("[onbook] Skipping broken story file:", fileName, msg);
1329
+ return [];
1326
1330
  }
1327
1331
  }
1328
- return plugins;
1329
- }
1332
+ };
1330
1333
 
1331
- export { AUTO_STORIES_FOLDER, storybookOnlookPlugin };
1334
+ 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 };