@onlook/storybook-plugin 0.4.0-beta.14 → 0.4.0-beta.16

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/cli/index.js CHANGED
@@ -63,6 +63,9 @@ var execFileAsync = promisify(execFile);
63
63
  var require2 = createRequire$1(import.meta.url);
64
64
  var browserPromise = null;
65
65
  var installPromise = null;
66
+ var ChromiumUnavailableError = class extends Error {
67
+ code = "CHROMIUM_UNAVAILABLE";
68
+ };
66
69
  var isMissingExecutableError = (err) => {
67
70
  const msg = err instanceof Error ? err.message : String(err);
68
71
  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.
@@ -95,7 +98,9 @@ async function installChromium() {
95
98
  } catch (err) {
96
99
  const detail = err instanceof Error ? err.message : String(err);
97
100
  console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
98
- throw new Error(`Playwright chromium install failed: ${detail}`);
101
+ throw new ChromiumUnavailableError(
102
+ `Playwright chromium install failed: ${detail}`
103
+ );
99
104
  } finally {
100
105
  installPromise = null;
101
106
  }
@@ -117,7 +122,7 @@ async function launchWithSelfHeal() {
117
122
  console.error(
118
123
  `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
119
124
  );
120
- throw new Error(
125
+ throw new ChromiumUnavailableError(
121
126
  `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
122
127
  );
123
128
  }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'node:module';
2
- import path7, { dirname, join, relative } from 'path';
2
+ import path8, { dirname, join, relative } from 'path';
3
3
  import crypto, { createHash } from 'crypto';
4
4
  import fs5, { existsSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
@@ -61,7 +61,7 @@ function envContainmentPlugin(options) {
61
61
  const resolveOutcomes = /* @__PURE__ */ new Map();
62
62
  const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
63
63
  const matchTarget = (absPath) => {
64
- const norm = normalizeSlashes(path7.normalize(absPath));
64
+ const norm = normalizeSlashes(path8.normalize(absPath));
65
65
  for (const rel of byModule.keys()) {
66
66
  for (const root of roots) {
67
67
  const target = `${root}/${rel}`;
@@ -96,10 +96,10 @@ function envContainmentPlugin(options) {
96
96
  if (source.startsWith("\0")) return null;
97
97
  const cleaned = stripQuery(source);
98
98
  let direct = null;
99
- if (path7.isAbsolute(cleaned)) {
99
+ if (path8.isAbsolute(cleaned)) {
100
100
  direct = cleaned;
101
101
  } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
102
- direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
102
+ direct = path8.resolve(path8.dirname(stripQuery(importer)), cleaned);
103
103
  }
104
104
  if (direct) {
105
105
  const rel = matchTarget(direct);
@@ -108,7 +108,7 @@ function envContainmentPlugin(options) {
108
108
  }
109
109
  if (!specifierMightMatch(cleaned)) return null;
110
110
  if (typeof this.resolve !== "function") return null;
111
- const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
111
+ const importerDir = importer ? path8.dirname(stripQuery(importer)) : "";
112
112
  const outcomeKey = `${importerDir}|${source}`;
113
113
  const memoized = resolveOutcomes.get(outcomeKey);
114
114
  if (memoized !== void 0) return memoized;
@@ -328,7 +328,7 @@ function componentLocPlugin(options = {}) {
328
328
  sourceFilename: filepath
329
329
  });
330
330
  let mutated = false;
331
- const relativePath = path7.relative(root, filepath);
331
+ const relativePath = path8.relative(root, filepath);
332
332
  traverse(ast, {
333
333
  JSXElement(nodePath) {
334
334
  const opening = nodePath.node.openingElement;
@@ -387,9 +387,9 @@ function fastRefreshTolerantExportsPlugin() {
387
387
  }
388
388
  };
389
389
  }
390
- var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
391
- var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
392
- var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
390
+ var CACHE_DIR = path8.join(process.cwd(), ".storybook-cache");
391
+ var SCREENSHOTS_DIR = path8.join(CACHE_DIR, "screenshots");
392
+ var MANIFEST_PATH = path8.join(CACHE_DIR, "manifest.json");
393
393
  var VIEWPORT_WIDTH = 1920;
394
394
  var VIEWPORT_HEIGHT = 1080;
395
395
  var MIN_COMPONENT_WIDTH = 420;
@@ -440,6 +440,9 @@ var execFileAsync = promisify(execFile);
440
440
  var require2 = createRequire$1(import.meta.url);
441
441
  var browserPromise = null;
442
442
  var installPromise = null;
443
+ var ChromiumUnavailableError = class extends Error {
444
+ code = "CHROMIUM_UNAVAILABLE";
445
+ };
443
446
  var isMissingExecutableError = (err) => {
444
447
  const msg = err instanceof Error ? err.message : String(err);
445
448
  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.
@@ -472,7 +475,9 @@ async function installChromium() {
472
475
  } catch (err) {
473
476
  const detail = err instanceof Error ? err.message : String(err);
474
477
  console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
475
- throw new Error(`Playwright chromium install failed: ${detail}`);
478
+ throw new ChromiumUnavailableError(
479
+ `Playwright chromium install failed: ${detail}`
480
+ );
476
481
  } finally {
477
482
  installPromise = null;
478
483
  }
@@ -494,7 +499,7 @@ async function launchWithSelfHeal() {
494
499
  console.error(
495
500
  `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
496
501
  );
497
- throw new Error(
502
+ throw new ChromiumUnavailableError(
498
503
  `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
499
504
  );
500
505
  }
@@ -575,8 +580,8 @@ async function detectRenderState(page, opts = {}) {
575
580
  }
576
581
  }
577
582
  function getScreenshotPath(storyId, theme) {
578
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
579
- return path7.join(storyDir, `${theme}.png`);
583
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
584
+ return path8.join(storyDir, `${theme}.png`);
580
585
  }
581
586
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", loadTimeoutMs = 9e4, renderTimeoutMs = 2e4) {
582
587
  const browser = await getBrowser();
@@ -664,7 +669,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
664
669
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
665
670
  try {
666
671
  ensureCacheDirectories();
667
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
672
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
668
673
  if (!fs5.existsSync(storyDir)) {
669
674
  fs5.mkdirSync(storyDir, { recursive: true });
670
675
  }
@@ -785,7 +790,7 @@ async function fetchStorybookIndex() {
785
790
  }
786
791
  function getStoriesForFile(filePath) {
787
792
  if (!cachedIndex) return [];
788
- const fileName = path7.basename(filePath);
793
+ const fileName = path8.basename(filePath);
789
794
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
790
795
  }
791
796
  async function regenerateScreenshotsForFiles(files) {
@@ -980,8 +985,8 @@ function softStoryRerenderPlugin() {
980
985
  preview.onStoriesChanged({ importFn: newModule.importFn });
981
986
  }
982
987
  });`;
983
- const out = code.replace(pattern, (_match, quote, path8) => {
984
- return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
988
+ const out = code.replace(pattern, (_match, quote, path9) => {
989
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path9}${quote}`);
985
990
  });
986
991
  return out === code ? null : { code: out, map: null };
987
992
  }
@@ -992,14 +997,14 @@ function resolveTsconfigAliases(root) {
992
997
  try {
993
998
  const config = readTsconfigAliasConfig(root);
994
999
  if (!config) return [];
995
- const base = path7.resolve(root, config.baseUrl ?? ".");
1000
+ const base = path8.resolve(root, config.baseUrl ?? ".");
996
1001
  const aliases = [];
997
1002
  for (const [key, targets] of Object.entries(config.paths)) {
998
1003
  const target = targets[0];
999
1004
  if (!target) continue;
1000
1005
  if (key.endsWith("/*") && target.endsWith("/*")) {
1001
1006
  const findPrefix = key.slice(0, -1);
1002
- const targetDir = path7.resolve(base, target.slice(0, -2));
1007
+ const targetDir = path8.resolve(base, target.slice(0, -2));
1003
1008
  aliases.push({
1004
1009
  find: new RegExp(`^${escapeRegExp(findPrefix)}`),
1005
1010
  replacement: `${targetDir}/`
@@ -1007,7 +1012,7 @@ function resolveTsconfigAliases(root) {
1007
1012
  } else if (!key.includes("*") && !target.includes("*")) {
1008
1013
  aliases.push({
1009
1014
  find: new RegExp(`^${escapeRegExp(key)}$`),
1010
- replacement: path7.resolve(base, target)
1015
+ replacement: path8.resolve(base, target)
1011
1016
  });
1012
1017
  }
1013
1018
  }
@@ -1018,7 +1023,7 @@ function resolveTsconfigAliases(root) {
1018
1023
  }
1019
1024
  function readTsconfigAliasConfig(root) {
1020
1025
  for (const candidate of TSCONFIG_CANDIDATES) {
1021
- const abs = path7.join(root, candidate);
1026
+ const abs = path8.join(root, candidate);
1022
1027
  let text;
1023
1028
  try {
1024
1029
  text = fs5.readFileSync(abs, "utf-8");
@@ -1099,8 +1104,128 @@ function findGitRoot(startPath) {
1099
1104
  }
1100
1105
  return null;
1101
1106
  }
1107
+ var STORY_FILE_PATTERN = /\.stories\.(js|jsx|mjs|ts|tsx|mdx)$/;
1108
+ function isStoryFile(file) {
1109
+ return STORY_FILE_PATTERN.test(file);
1110
+ }
1111
+ function missingFromIndex(index, files) {
1112
+ const importPaths = [];
1113
+ for (const entry of Object.values(index.entries ?? {})) {
1114
+ if (typeof entry.importPath === "string") importPaths.push(entry.importPath);
1115
+ }
1116
+ return files.filter((file) => {
1117
+ const base = path8.basename(file);
1118
+ return !importPaths.some((importPath) => importPath.endsWith(base));
1119
+ });
1120
+ }
1121
+ function portFromArgv(argv) {
1122
+ for (let i = 0; i < argv.length; i += 1) {
1123
+ const arg = argv[i];
1124
+ if (arg === void 0) continue;
1125
+ let raw;
1126
+ if (arg === "-p" || arg === "--port") {
1127
+ raw = argv[i + 1];
1128
+ } else {
1129
+ raw = /^(?:-p|--port)=(\d+)$/.exec(arg)?.[1];
1130
+ }
1131
+ if (raw === void 0) continue;
1132
+ const parsed = Number.parseInt(raw, 10);
1133
+ if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) return parsed;
1134
+ }
1135
+ return null;
1136
+ }
1137
+ async function defaultFetchIndex(port) {
1138
+ try {
1139
+ const res = await fetch(`http://localhost:${port}/index.json?_t=${Date.now()}`, {
1140
+ cache: "no-store",
1141
+ signal: AbortSignal.timeout(2e3)
1142
+ });
1143
+ if (!res.ok) return null;
1144
+ return await res.json();
1145
+ } catch {
1146
+ return null;
1147
+ }
1148
+ }
1149
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1150
+ function createStoryIndexBroadcaster({
1151
+ ports,
1152
+ notify,
1153
+ fetchIndex = defaultFetchIndex,
1154
+ debounceMs = 500,
1155
+ probeIntervalMs = 500,
1156
+ probeCeilingMs = 15e3
1157
+ }) {
1158
+ const candidatePorts = [...new Set(ports)];
1159
+ const pendingAdds = /* @__PURE__ */ new Set();
1160
+ let addDebounce = null;
1161
+ let unlinkDebounce = null;
1162
+ let probing = false;
1163
+ const scheduleProbe = () => {
1164
+ if (addDebounce) clearTimeout(addDebounce);
1165
+ addDebounce = setTimeout(() => {
1166
+ addDebounce = null;
1167
+ void probeThenNotify();
1168
+ }, debounceMs);
1169
+ };
1170
+ const probeThenNotify = async () => {
1171
+ if (probing) return;
1172
+ probing = true;
1173
+ const files = Array.from(pendingAdds);
1174
+ const deadline = Date.now() + probeCeilingMs;
1175
+ try {
1176
+ if (files.length === 0) return;
1177
+ while (Date.now() < deadline) {
1178
+ for (const candidate of candidatePorts) {
1179
+ const index = await fetchIndex(candidate);
1180
+ if (index && missingFromIndex(index, files).length === 0) {
1181
+ console.log(
1182
+ "[STORYBOOK_PLUGIN] Index caught up with new story files \u2014 broadcasting",
1183
+ { files, port: candidate }
1184
+ );
1185
+ for (const file of files) pendingAdds.delete(file);
1186
+ notify();
1187
+ return;
1188
+ }
1189
+ }
1190
+ await sleep(probeIntervalMs);
1191
+ }
1192
+ console.warn("[STORYBOOK_PLUGIN] Index probe hit ceiling \u2014 broadcasting anyway", {
1193
+ files,
1194
+ probeCeilingMs
1195
+ });
1196
+ for (const file of files) pendingAdds.delete(file);
1197
+ notify();
1198
+ } finally {
1199
+ probing = false;
1200
+ if (pendingAdds.size > 0) scheduleProbe();
1201
+ }
1202
+ };
1203
+ return {
1204
+ onStoryFileAdded(file) {
1205
+ if (!isStoryFile(file)) return;
1206
+ pendingAdds.add(file);
1207
+ scheduleProbe();
1208
+ },
1209
+ onStoryFileRemoved(file) {
1210
+ if (!isStoryFile(file)) return;
1211
+ pendingAdds.delete(file);
1212
+ if (unlinkDebounce) clearTimeout(unlinkDebounce);
1213
+ unlinkDebounce = setTimeout(() => {
1214
+ unlinkDebounce = null;
1215
+ console.log("[STORYBOOK_PLUGIN] Story file removed \u2014 broadcasting", { file });
1216
+ notify();
1217
+ }, debounceMs);
1218
+ }
1219
+ };
1220
+ }
1102
1221
 
1103
1222
  // src/storybook-onlook-plugin.ts
1223
+ function errorCode(error) {
1224
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1225
+ return error.code;
1226
+ }
1227
+ return void 0;
1228
+ }
1104
1229
  var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1105
1230
  var storybookDir = join(__dirname$1, "..");
1106
1231
  var gitRoot = findGitRoot(storybookDir);
@@ -1159,7 +1284,7 @@ var TAILWIND_ENTRY_CANDIDATES = [
1159
1284
  var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
1160
1285
  function detectTailwindEntryCss(cwd) {
1161
1286
  for (const rel of TAILWIND_ENTRY_CANDIDATES) {
1162
- const abs = path7.join(cwd, rel);
1287
+ const abs = path8.join(cwd, rel);
1163
1288
  try {
1164
1289
  if (!fs5.existsSync(abs)) continue;
1165
1290
  const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
@@ -1246,11 +1371,13 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1246
1371
  res.end(buffer);
1247
1372
  }).catch((error) => {
1248
1373
  console.error("Screenshot capture error:", error);
1374
+ const code = errorCode(error);
1249
1375
  res.statusCode = 500;
1250
1376
  res.setHeader("Content-Type", "application/json");
1251
1377
  res.end(
1252
1378
  JSON.stringify({
1253
1379
  error: "Failed to capture screenshot",
1380
+ ...code ? { code } : {},
1254
1381
  details: String(error)
1255
1382
  })
1256
1383
  );
@@ -1280,11 +1407,13 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1280
1407
  res.end(JSON.stringify(result));
1281
1408
  }).catch((error) => {
1282
1409
  console.error("Console log capture error:", error);
1410
+ const code = errorCode(error);
1283
1411
  res.statusCode = 500;
1284
1412
  res.setHeader("Content-Type", "application/json");
1285
1413
  res.end(
1286
1414
  JSON.stringify({
1287
1415
  error: "Failed to capture console logs",
1416
+ ...code ? { code } : {},
1288
1417
  details: String(error)
1289
1418
  })
1290
1419
  );
@@ -1292,7 +1421,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1292
1421
  return;
1293
1422
  }
1294
1423
  if (req.url?.startsWith("/screenshots/")) {
1295
- const screenshotPath = path7.join(
1424
+ const screenshotPath = path8.join(
1296
1425
  process.cwd(),
1297
1426
  ".storybook-cache",
1298
1427
  req.url.replace("/screenshots/", "screenshots/")
@@ -1313,7 +1442,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1313
1442
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1314
1443
  );
1315
1444
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1316
- const storyDir = path7.join(
1445
+ const storyDir = path8.join(
1317
1446
  process.cwd(),
1318
1447
  ".storybook-cache",
1319
1448
  "screenshots",
@@ -1332,6 +1461,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1332
1461
  `[STORYBOOK_PLUGIN] Failed to generate screenshot: ${storyId}/${theme}`,
1333
1462
  error
1334
1463
  );
1464
+ const code = errorCode(error);
1335
1465
  res.statusCode = 500;
1336
1466
  res.setHeader("Content-Type", "application/json");
1337
1467
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -1339,6 +1469,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1339
1469
  res.end(
1340
1470
  JSON.stringify({
1341
1471
  error: "Failed to generate screenshot",
1472
+ ...code ? { code } : {},
1342
1473
  details: String(error)
1343
1474
  })
1344
1475
  );
@@ -1430,7 +1561,7 @@ function storybookOnlookPlugin(options = {}) {
1430
1561
  configureServer(server) {
1431
1562
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1432
1563
  server.middlewares.use(serveMetadataAndScreenshots);
1433
- const cachedManifestPath = path7.join(
1564
+ const cachedManifestPath = path8.join(
1434
1565
  process.cwd(),
1435
1566
  ".storybook-cache",
1436
1567
  "manifest.json"
@@ -1449,7 +1580,7 @@ function storybookOnlookPlugin(options = {}) {
1449
1580
  server.watcher.on("change", scheduleRefresh);
1450
1581
  const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1451
1582
  const resolvedStoryGlobs = storyGlobs.map(
1452
- (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1583
+ (glob) => path8.isAbsolute(glob) ? glob : path8.join(process.cwd(), glob)
1453
1584
  );
1454
1585
  if (resolvedStoryGlobs.length > 0) {
1455
1586
  server.watcher.add(resolvedStoryGlobs);
@@ -1457,10 +1588,36 @@ function storybookOnlookPlugin(options = {}) {
1457
1588
  storyGlobs: resolvedStoryGlobs
1458
1589
  });
1459
1590
  }
1591
+ const probeCandidatePorts = [
1592
+ ...new Set(
1593
+ [portFromArgv(process.argv), port, server.config.server.port].filter(
1594
+ (p) => typeof p === "number"
1595
+ )
1596
+ )
1597
+ ];
1598
+ console.log("[STORYBOOK_PLUGIN] Story add/unlink index broadcast enabled", {
1599
+ probeCandidatePorts
1600
+ });
1601
+ const indexBroadcaster = createStoryIndexBroadcaster({
1602
+ ports: probeCandidatePorts,
1603
+ notify: notifyOnbookIndexChanged
1604
+ });
1605
+ let indexBroadcastWatcherReady = false;
1606
+ server.watcher.once("ready", () => {
1607
+ indexBroadcastWatcherReady = true;
1608
+ });
1609
+ server.watcher.on("add", (file) => {
1610
+ if (!indexBroadcastWatcherReady) return;
1611
+ indexBroadcaster.onStoryFileAdded(file);
1612
+ });
1613
+ server.watcher.on("unlink", (file) => {
1614
+ if (!indexBroadcastWatcherReady) return;
1615
+ indexBroadcaster.onStoryFileRemoved(file);
1616
+ });
1460
1617
  const tailwindEntryPath = (() => {
1461
1618
  if (options.tailwindEntryCss === false) return null;
1462
1619
  if (typeof options.tailwindEntryCss === "string") {
1463
- return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1620
+ return path8.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path8.join(process.cwd(), options.tailwindEntryCss);
1464
1621
  }
1465
1622
  return detectTailwindEntryCss(process.cwd());
1466
1623
  })();
@@ -1476,7 +1633,7 @@ function storybookOnlookPlugin(options = {}) {
1476
1633
  });
1477
1634
  server.watcher.on("add", (file) => {
1478
1635
  if (!/\.tsx?$/.test(file)) return;
1479
- const dir = path7.dirname(file);
1636
+ const dir = path8.dirname(file);
1480
1637
  const isNewDir = !seenDirs.has(dir);
1481
1638
  seenDirs.add(dir);
1482
1639
  if (!watcherReady || !isNewDir) return;
@@ -1507,7 +1664,7 @@ function storybookOnlookPlugin(options = {}) {
1507
1664
  configurePreviewServer(server) {
1508
1665
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1509
1666
  server.middlewares.use(serveMetadataAndScreenshots);
1510
- refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
1667
+ refreshManifest(path8.join(process.cwd(), ".storybook-cache", "manifest.json"));
1511
1668
  },
1512
1669
  handleHotUpdate: handleStoryFileChange
1513
1670
  };
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import crypto, { createHash } from 'crypto';
3
3
  import fs5, { existsSync } from 'fs';
4
- import path7, { dirname, join, relative } from 'path';
4
+ import path8, { dirname, join, relative } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import generateModule from '@babel/generator';
7
7
  import { parse } from '@babel/parser';
@@ -45,7 +45,7 @@ function componentLocPlugin(options = {}) {
45
45
  sourceFilename: filepath
46
46
  });
47
47
  let mutated = false;
48
- const relativePath = path7.relative(root, filepath);
48
+ const relativePath = path8.relative(root, filepath);
49
49
  traverse(ast, {
50
50
  JSXElement(nodePath) {
51
51
  const opening = nodePath.node.openingElement;
@@ -117,7 +117,7 @@ function envContainmentPlugin(options) {
117
117
  const resolveOutcomes = /* @__PURE__ */ new Map();
118
118
  const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
119
119
  const matchTarget = (absPath) => {
120
- const norm = normalizeSlashes(path7.normalize(absPath));
120
+ const norm = normalizeSlashes(path8.normalize(absPath));
121
121
  for (const rel of byModule.keys()) {
122
122
  for (const root of roots) {
123
123
  const target = `${root}/${rel}`;
@@ -152,10 +152,10 @@ function envContainmentPlugin(options) {
152
152
  if (source.startsWith("\0")) return null;
153
153
  const cleaned = stripQuery(source);
154
154
  let direct = null;
155
- if (path7.isAbsolute(cleaned)) {
155
+ if (path8.isAbsolute(cleaned)) {
156
156
  direct = cleaned;
157
157
  } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
158
- direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
158
+ direct = path8.resolve(path8.dirname(stripQuery(importer)), cleaned);
159
159
  }
160
160
  if (direct) {
161
161
  const rel = matchTarget(direct);
@@ -164,7 +164,7 @@ function envContainmentPlugin(options) {
164
164
  }
165
165
  if (!specifierMightMatch(cleaned)) return null;
166
166
  if (typeof this.resolve !== "function") return null;
167
- const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
167
+ const importerDir = importer ? path8.dirname(stripQuery(importer)) : "";
168
168
  const outcomeKey = `${importerDir}|${source}`;
169
169
  const memoized = resolveOutcomes.get(outcomeKey);
170
170
  if (memoized !== void 0) return memoized;
@@ -267,9 +267,9 @@ function fastRefreshTolerantExportsPlugin() {
267
267
  }
268
268
  };
269
269
  }
270
- var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
271
- var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
272
- var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
270
+ var CACHE_DIR = path8.join(process.cwd(), ".storybook-cache");
271
+ var SCREENSHOTS_DIR = path8.join(CACHE_DIR, "screenshots");
272
+ var MANIFEST_PATH = path8.join(CACHE_DIR, "manifest.json");
273
273
  var VIEWPORT_WIDTH = 1920;
274
274
  var VIEWPORT_HEIGHT = 1080;
275
275
  var MIN_COMPONENT_WIDTH = 420;
@@ -320,6 +320,9 @@ var execFileAsync = promisify(execFile);
320
320
  var require2 = createRequire$1(import.meta.url);
321
321
  var browserPromise = null;
322
322
  var installPromise = null;
323
+ var ChromiumUnavailableError = class extends Error {
324
+ code = "CHROMIUM_UNAVAILABLE";
325
+ };
323
326
  var isMissingExecutableError = (err) => {
324
327
  const msg = err instanceof Error ? err.message : String(err);
325
328
  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.
@@ -352,7 +355,9 @@ async function installChromium() {
352
355
  } catch (err) {
353
356
  const detail = err instanceof Error ? err.message : String(err);
354
357
  console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
355
- throw new Error(`Playwright chromium install failed: ${detail}`);
358
+ throw new ChromiumUnavailableError(
359
+ `Playwright chromium install failed: ${detail}`
360
+ );
356
361
  } finally {
357
362
  installPromise = null;
358
363
  }
@@ -374,7 +379,7 @@ async function launchWithSelfHeal() {
374
379
  console.error(
375
380
  `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
376
381
  );
377
- throw new Error(
382
+ throw new ChromiumUnavailableError(
378
383
  `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
379
384
  );
380
385
  }
@@ -455,8 +460,8 @@ async function detectRenderState(page, opts = {}) {
455
460
  }
456
461
  }
457
462
  function getScreenshotPath(storyId, theme) {
458
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
459
- return path7.join(storyDir, `${theme}.png`);
463
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
464
+ return path8.join(storyDir, `${theme}.png`);
460
465
  }
461
466
  async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", loadTimeoutMs = 9e4, renderTimeoutMs = 2e4) {
462
467
  const browser = await getBrowser();
@@ -544,7 +549,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
544
549
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
545
550
  try {
546
551
  ensureCacheDirectories();
547
- const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
552
+ const storyDir = path8.join(SCREENSHOTS_DIR, storyId);
548
553
  if (!fs5.existsSync(storyDir)) {
549
554
  fs5.mkdirSync(storyDir, { recursive: true });
550
555
  }
@@ -665,7 +670,7 @@ async function fetchStorybookIndex() {
665
670
  }
666
671
  function getStoriesForFile(filePath) {
667
672
  if (!cachedIndex) return [];
668
- const fileName = path7.basename(filePath);
673
+ const fileName = path8.basename(filePath);
669
674
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
670
675
  }
671
676
  async function regenerateScreenshotsForFiles(files) {
@@ -976,8 +981,8 @@ function softStoryRerenderPlugin() {
976
981
  preview.onStoriesChanged({ importFn: newModule.importFn });
977
982
  }
978
983
  });`;
979
- const out = code.replace(pattern, (_match, quote, path8) => {
980
- return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
984
+ const out = code.replace(pattern, (_match, quote, path9) => {
985
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path9}${quote}`);
981
986
  });
982
987
  return out === code ? null : { code: out, map: null };
983
988
  }
@@ -988,14 +993,14 @@ function resolveTsconfigAliases(root) {
988
993
  try {
989
994
  const config = readTsconfigAliasConfig(root);
990
995
  if (!config) return [];
991
- const base = path7.resolve(root, config.baseUrl ?? ".");
996
+ const base = path8.resolve(root, config.baseUrl ?? ".");
992
997
  const aliases = [];
993
998
  for (const [key, targets] of Object.entries(config.paths)) {
994
999
  const target = targets[0];
995
1000
  if (!target) continue;
996
1001
  if (key.endsWith("/*") && target.endsWith("/*")) {
997
1002
  const findPrefix = key.slice(0, -1);
998
- const targetDir = path7.resolve(base, target.slice(0, -2));
1003
+ const targetDir = path8.resolve(base, target.slice(0, -2));
999
1004
  aliases.push({
1000
1005
  find: new RegExp(`^${escapeRegExp(findPrefix)}`),
1001
1006
  replacement: `${targetDir}/`
@@ -1003,7 +1008,7 @@ function resolveTsconfigAliases(root) {
1003
1008
  } else if (!key.includes("*") && !target.includes("*")) {
1004
1009
  aliases.push({
1005
1010
  find: new RegExp(`^${escapeRegExp(key)}$`),
1006
- replacement: path7.resolve(base, target)
1011
+ replacement: path8.resolve(base, target)
1007
1012
  });
1008
1013
  }
1009
1014
  }
@@ -1014,7 +1019,7 @@ function resolveTsconfigAliases(root) {
1014
1019
  }
1015
1020
  function readTsconfigAliasConfig(root) {
1016
1021
  for (const candidate of TSCONFIG_CANDIDATES) {
1017
- const abs = path7.join(root, candidate);
1022
+ const abs = path8.join(root, candidate);
1018
1023
  let text;
1019
1024
  try {
1020
1025
  text = fs5.readFileSync(abs, "utf-8");
@@ -1095,8 +1100,128 @@ function findGitRoot(startPath) {
1095
1100
  }
1096
1101
  return null;
1097
1102
  }
1103
+ var STORY_FILE_PATTERN = /\.stories\.(js|jsx|mjs|ts|tsx|mdx)$/;
1104
+ function isStoryFile(file) {
1105
+ return STORY_FILE_PATTERN.test(file);
1106
+ }
1107
+ function missingFromIndex(index, files) {
1108
+ const importPaths = [];
1109
+ for (const entry of Object.values(index.entries ?? {})) {
1110
+ if (typeof entry.importPath === "string") importPaths.push(entry.importPath);
1111
+ }
1112
+ return files.filter((file) => {
1113
+ const base = path8.basename(file);
1114
+ return !importPaths.some((importPath) => importPath.endsWith(base));
1115
+ });
1116
+ }
1117
+ function portFromArgv(argv) {
1118
+ for (let i = 0; i < argv.length; i += 1) {
1119
+ const arg = argv[i];
1120
+ if (arg === void 0) continue;
1121
+ let raw;
1122
+ if (arg === "-p" || arg === "--port") {
1123
+ raw = argv[i + 1];
1124
+ } else {
1125
+ raw = /^(?:-p|--port)=(\d+)$/.exec(arg)?.[1];
1126
+ }
1127
+ if (raw === void 0) continue;
1128
+ const parsed = Number.parseInt(raw, 10);
1129
+ if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) return parsed;
1130
+ }
1131
+ return null;
1132
+ }
1133
+ async function defaultFetchIndex(port) {
1134
+ try {
1135
+ const res = await fetch(`http://localhost:${port}/index.json?_t=${Date.now()}`, {
1136
+ cache: "no-store",
1137
+ signal: AbortSignal.timeout(2e3)
1138
+ });
1139
+ if (!res.ok) return null;
1140
+ return await res.json();
1141
+ } catch {
1142
+ return null;
1143
+ }
1144
+ }
1145
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1146
+ function createStoryIndexBroadcaster({
1147
+ ports,
1148
+ notify,
1149
+ fetchIndex = defaultFetchIndex,
1150
+ debounceMs = 500,
1151
+ probeIntervalMs = 500,
1152
+ probeCeilingMs = 15e3
1153
+ }) {
1154
+ const candidatePorts = [...new Set(ports)];
1155
+ const pendingAdds = /* @__PURE__ */ new Set();
1156
+ let addDebounce = null;
1157
+ let unlinkDebounce = null;
1158
+ let probing = false;
1159
+ const scheduleProbe = () => {
1160
+ if (addDebounce) clearTimeout(addDebounce);
1161
+ addDebounce = setTimeout(() => {
1162
+ addDebounce = null;
1163
+ void probeThenNotify();
1164
+ }, debounceMs);
1165
+ };
1166
+ const probeThenNotify = async () => {
1167
+ if (probing) return;
1168
+ probing = true;
1169
+ const files = Array.from(pendingAdds);
1170
+ const deadline = Date.now() + probeCeilingMs;
1171
+ try {
1172
+ if (files.length === 0) return;
1173
+ while (Date.now() < deadline) {
1174
+ for (const candidate of candidatePorts) {
1175
+ const index = await fetchIndex(candidate);
1176
+ if (index && missingFromIndex(index, files).length === 0) {
1177
+ console.log(
1178
+ "[STORYBOOK_PLUGIN] Index caught up with new story files \u2014 broadcasting",
1179
+ { files, port: candidate }
1180
+ );
1181
+ for (const file of files) pendingAdds.delete(file);
1182
+ notify();
1183
+ return;
1184
+ }
1185
+ }
1186
+ await sleep(probeIntervalMs);
1187
+ }
1188
+ console.warn("[STORYBOOK_PLUGIN] Index probe hit ceiling \u2014 broadcasting anyway", {
1189
+ files,
1190
+ probeCeilingMs
1191
+ });
1192
+ for (const file of files) pendingAdds.delete(file);
1193
+ notify();
1194
+ } finally {
1195
+ probing = false;
1196
+ if (pendingAdds.size > 0) scheduleProbe();
1197
+ }
1198
+ };
1199
+ return {
1200
+ onStoryFileAdded(file) {
1201
+ if (!isStoryFile(file)) return;
1202
+ pendingAdds.add(file);
1203
+ scheduleProbe();
1204
+ },
1205
+ onStoryFileRemoved(file) {
1206
+ if (!isStoryFile(file)) return;
1207
+ pendingAdds.delete(file);
1208
+ if (unlinkDebounce) clearTimeout(unlinkDebounce);
1209
+ unlinkDebounce = setTimeout(() => {
1210
+ unlinkDebounce = null;
1211
+ console.log("[STORYBOOK_PLUGIN] Story file removed \u2014 broadcasting", { file });
1212
+ notify();
1213
+ }, debounceMs);
1214
+ }
1215
+ };
1216
+ }
1098
1217
 
1099
1218
  // src/storybook-onlook-plugin.ts
1219
+ function errorCode(error) {
1220
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
1221
+ return error.code;
1222
+ }
1223
+ return void 0;
1224
+ }
1100
1225
  var __dirname$1 = dirname(fileURLToPath(import.meta.url));
1101
1226
  var storybookDir = join(__dirname$1, "..");
1102
1227
  var gitRoot = findGitRoot(storybookDir);
@@ -1155,7 +1280,7 @@ var TAILWIND_ENTRY_CANDIDATES = [
1155
1280
  var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
1156
1281
  function detectTailwindEntryCss(cwd) {
1157
1282
  for (const rel of TAILWIND_ENTRY_CANDIDATES) {
1158
- const abs = path7.join(cwd, rel);
1283
+ const abs = path8.join(cwd, rel);
1159
1284
  try {
1160
1285
  if (!fs5.existsSync(abs)) continue;
1161
1286
  const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
@@ -1242,11 +1367,13 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1242
1367
  res.end(buffer);
1243
1368
  }).catch((error) => {
1244
1369
  console.error("Screenshot capture error:", error);
1370
+ const code = errorCode(error);
1245
1371
  res.statusCode = 500;
1246
1372
  res.setHeader("Content-Type", "application/json");
1247
1373
  res.end(
1248
1374
  JSON.stringify({
1249
1375
  error: "Failed to capture screenshot",
1376
+ ...code ? { code } : {},
1250
1377
  details: String(error)
1251
1378
  })
1252
1379
  );
@@ -1276,11 +1403,13 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1276
1403
  res.end(JSON.stringify(result));
1277
1404
  }).catch((error) => {
1278
1405
  console.error("Console log capture error:", error);
1406
+ const code = errorCode(error);
1279
1407
  res.statusCode = 500;
1280
1408
  res.setHeader("Content-Type", "application/json");
1281
1409
  res.end(
1282
1410
  JSON.stringify({
1283
1411
  error: "Failed to capture console logs",
1412
+ ...code ? { code } : {},
1284
1413
  details: String(error)
1285
1414
  })
1286
1415
  );
@@ -1288,7 +1417,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1288
1417
  return;
1289
1418
  }
1290
1419
  if (req.url?.startsWith("/screenshots/")) {
1291
- const screenshotPath = path7.join(
1420
+ const screenshotPath = path8.join(
1292
1421
  process.cwd(),
1293
1422
  ".storybook-cache",
1294
1423
  req.url.replace("/screenshots/", "screenshots/")
@@ -1309,7 +1438,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1309
1438
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1310
1439
  );
1311
1440
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1312
- const storyDir = path7.join(
1441
+ const storyDir = path8.join(
1313
1442
  process.cwd(),
1314
1443
  ".storybook-cache",
1315
1444
  "screenshots",
@@ -1328,6 +1457,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1328
1457
  `[STORYBOOK_PLUGIN] Failed to generate screenshot: ${storyId}/${theme}`,
1329
1458
  error
1330
1459
  );
1460
+ const code = errorCode(error);
1331
1461
  res.statusCode = 500;
1332
1462
  res.setHeader("Content-Type", "application/json");
1333
1463
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -1335,6 +1465,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1335
1465
  res.end(
1336
1466
  JSON.stringify({
1337
1467
  error: "Failed to generate screenshot",
1468
+ ...code ? { code } : {},
1338
1469
  details: String(error)
1339
1470
  })
1340
1471
  );
@@ -1426,7 +1557,7 @@ function storybookOnlookPlugin(options = {}) {
1426
1557
  configureServer(server) {
1427
1558
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1428
1559
  server.middlewares.use(serveMetadataAndScreenshots);
1429
- const cachedManifestPath = path7.join(
1560
+ const cachedManifestPath = path8.join(
1430
1561
  process.cwd(),
1431
1562
  ".storybook-cache",
1432
1563
  "manifest.json"
@@ -1445,7 +1576,7 @@ function storybookOnlookPlugin(options = {}) {
1445
1576
  server.watcher.on("change", scheduleRefresh);
1446
1577
  const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1447
1578
  const resolvedStoryGlobs = storyGlobs.map(
1448
- (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1579
+ (glob) => path8.isAbsolute(glob) ? glob : path8.join(process.cwd(), glob)
1449
1580
  );
1450
1581
  if (resolvedStoryGlobs.length > 0) {
1451
1582
  server.watcher.add(resolvedStoryGlobs);
@@ -1453,10 +1584,36 @@ function storybookOnlookPlugin(options = {}) {
1453
1584
  storyGlobs: resolvedStoryGlobs
1454
1585
  });
1455
1586
  }
1587
+ const probeCandidatePorts = [
1588
+ ...new Set(
1589
+ [portFromArgv(process.argv), port, server.config.server.port].filter(
1590
+ (p) => typeof p === "number"
1591
+ )
1592
+ )
1593
+ ];
1594
+ console.log("[STORYBOOK_PLUGIN] Story add/unlink index broadcast enabled", {
1595
+ probeCandidatePorts
1596
+ });
1597
+ const indexBroadcaster = createStoryIndexBroadcaster({
1598
+ ports: probeCandidatePorts,
1599
+ notify: notifyOnbookIndexChanged
1600
+ });
1601
+ let indexBroadcastWatcherReady = false;
1602
+ server.watcher.once("ready", () => {
1603
+ indexBroadcastWatcherReady = true;
1604
+ });
1605
+ server.watcher.on("add", (file) => {
1606
+ if (!indexBroadcastWatcherReady) return;
1607
+ indexBroadcaster.onStoryFileAdded(file);
1608
+ });
1609
+ server.watcher.on("unlink", (file) => {
1610
+ if (!indexBroadcastWatcherReady) return;
1611
+ indexBroadcaster.onStoryFileRemoved(file);
1612
+ });
1456
1613
  const tailwindEntryPath = (() => {
1457
1614
  if (options.tailwindEntryCss === false) return null;
1458
1615
  if (typeof options.tailwindEntryCss === "string") {
1459
- return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1616
+ return path8.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path8.join(process.cwd(), options.tailwindEntryCss);
1460
1617
  }
1461
1618
  return detectTailwindEntryCss(process.cwd());
1462
1619
  })();
@@ -1472,7 +1629,7 @@ function storybookOnlookPlugin(options = {}) {
1472
1629
  });
1473
1630
  server.watcher.on("add", (file) => {
1474
1631
  if (!/\.tsx?$/.test(file)) return;
1475
- const dir = path7.dirname(file);
1632
+ const dir = path8.dirname(file);
1476
1633
  const isNewDir = !seenDirs.has(dir);
1477
1634
  seenDirs.add(dir);
1478
1635
  if (!watcherReady || !isNewDir) return;
@@ -1503,7 +1660,7 @@ function storybookOnlookPlugin(options = {}) {
1503
1660
  configurePreviewServer(server) {
1504
1661
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1505
1662
  server.middlewares.use(serveMetadataAndScreenshots);
1506
- refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
1663
+ refreshManifest(path8.join(process.cwd(), ".storybook-cache", "manifest.json"));
1507
1664
  },
1508
1665
  handleHotUpdate: handleStoryFileChange
1509
1666
  };
@@ -61,6 +61,9 @@ var execFileAsync = promisify(execFile);
61
61
  var require2 = createRequire$1(import.meta.url);
62
62
  var browserPromise = null;
63
63
  var installPromise = null;
64
+ var ChromiumUnavailableError = class extends Error {
65
+ code = "CHROMIUM_UNAVAILABLE";
66
+ };
64
67
  var isMissingExecutableError = (err) => {
65
68
  const msg = err instanceof Error ? err.message : String(err);
66
69
  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.
@@ -93,7 +96,9 @@ async function installChromium() {
93
96
  } catch (err) {
94
97
  const detail = err instanceof Error ? err.message : String(err);
95
98
  console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
96
- throw new Error(`Playwright chromium install failed: ${detail}`);
99
+ throw new ChromiumUnavailableError(
100
+ `Playwright chromium install failed: ${detail}`
101
+ );
97
102
  } finally {
98
103
  installPromise = null;
99
104
  }
@@ -115,7 +120,7 @@ async function launchWithSelfHeal() {
115
120
  console.error(
116
121
  `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
117
122
  );
118
- throw new Error(
123
+ throw new ChromiumUnavailableError(
119
124
  `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
120
125
  );
121
126
  }
@@ -1,6 +1,17 @@
1
1
  import { Browser } from 'playwright';
2
2
 
3
+ /**
4
+ * Terminal "this sandbox cannot launch chromium" failure (ONL-1429/ONL-1602).
5
+ * The `code` property rides the plugin's HTTP 5xx JSON error bodies
6
+ * (storybook-onlook-plugin.ts) so consumers — the agent runner's canvas
7
+ * tools — can classify the failure as tool_unavailable via a contract WE own,
8
+ * instead of regex-parsing Playwright's error prose (which reworded across
9
+ * versions and broke the detection twice: ONL-1085 → ONL-1364).
10
+ */
11
+ declare class ChromiumUnavailableError extends Error {
12
+ readonly code = "CHROMIUM_UNAVAILABLE";
13
+ }
3
14
  declare function getBrowser(): Promise<Browser>;
4
15
  declare function closeBrowser(): Promise<void>;
5
16
 
6
- export { closeBrowser, getBrowser };
17
+ export { ChromiumUnavailableError, closeBrowser, getBrowser };
@@ -10,6 +10,9 @@ var execFileAsync = promisify(execFile);
10
10
  var require2 = createRequire$1(import.meta.url);
11
11
  var browserPromise = null;
12
12
  var installPromise = null;
13
+ var ChromiumUnavailableError = class extends Error {
14
+ code = "CHROMIUM_UNAVAILABLE";
15
+ };
13
16
  var isMissingExecutableError = (err) => {
14
17
  const msg = err instanceof Error ? err.message : String(err);
15
18
  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.
@@ -42,7 +45,9 @@ async function installChromium() {
42
45
  } catch (err) {
43
46
  const detail = err instanceof Error ? err.message : String(err);
44
47
  console.error(`[STORYBOOK_PLUGIN] chromium install FAILED: ${detail}`);
45
- throw new Error(`Playwright chromium install failed: ${detail}`);
48
+ throw new ChromiumUnavailableError(
49
+ `Playwright chromium install failed: ${detail}`
50
+ );
46
51
  } finally {
47
52
  installPromise = null;
48
53
  }
@@ -64,7 +69,7 @@ async function launchWithSelfHeal() {
64
69
  console.error(
65
70
  `[STORYBOOK_PLUGIN] launch still failing after install \u2014 likely path or libs issue: ${detail}`
66
71
  );
67
- throw new Error(
72
+ throw new ChromiumUnavailableError(
68
73
  `Playwright chromium is unavailable in this sandbox (install ran but launch still failed): ${detail}`
69
74
  );
70
75
  }
@@ -91,4 +96,4 @@ async function closeBrowser() {
91
96
  }
92
97
  }
93
98
 
94
- export { closeBrowser, getBrowser };
99
+ export { ChromiumUnavailableError, closeBrowser, getBrowser };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlook/storybook-plugin",
3
- "version": "0.4.0-beta.14",
3
+ "version": "0.4.0-beta.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "onlook-storybook": "./dist/cli/index.js"
@@ -47,12 +47,12 @@
47
47
  "@babel/traverse": "^7.26.9",
48
48
  "@babel/types": "^7.29.0",
49
49
  "@drizzle-team/brocli": "^0.11.0",
50
- "playwright": "^1.52.0",
51
- "playwright-core": "^1.52.0",
50
+ "playwright": "1.57.0",
51
+ "playwright-core": "1.57.0",
52
52
  "storybook": "^10.1.11"
53
53
  },
54
54
  "devDependencies": {
55
- "@onbook/tsconfig": "0.1.0",
55
+ "@onbook/tsconfig": "workspace:*",
56
56
  "@types/babel__generator": "^7.6.8",
57
57
  "@types/babel__traverse": "^7.20.6",
58
58
  "@types/node": "^22.15.32",