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