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

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.
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import crypto, { createHash } from 'crypto';
3
- import fs4, { existsSync } from 'fs';
4
- import path6, { dirname, join, relative } from 'path';
3
+ import fs5, { existsSync } from 'fs';
4
+ import path7, { 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';
@@ -36,7 +36,7 @@ function componentLocPlugin(options = {}) {
36
36
  sourceFilename: filepath
37
37
  });
38
38
  let mutated = false;
39
- const relativePath = path6.relative(root, filepath);
39
+ const relativePath = path7.relative(root, filepath);
40
40
  traverse(ast, {
41
41
  JSXElement(nodePath) {
42
42
  const opening = nodePath.node.openingElement;
@@ -77,6 +77,8 @@ function componentLocPlugin(options = {}) {
77
77
  // src/containment-contract/containment-contract.ts
78
78
  var ONLOOK_CONTAINED_MODULES_GLOBAL = "__ONLOOK_CONTAINED_MODULES__";
79
79
  var ONLOOK_CONTAINED_PROP = "__onlookContained";
80
+ var ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
81
+ var ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
80
82
 
81
83
  // src/env-containment/env-containment.ts
82
84
  var ENV_CONTAINMENT_PLUGIN_NAME = "onlook-env-containment";
@@ -114,7 +116,7 @@ function envContainmentPlugin(options) {
114
116
  const resolveOutcomes = /* @__PURE__ */ new Map();
115
117
  const roots = /* @__PURE__ */ new Set([normalizeSlashes(process.cwd())]);
116
118
  const matchTarget = (absPath) => {
117
- const norm = normalizeSlashes(path6.normalize(absPath));
119
+ const norm = normalizeSlashes(path7.normalize(absPath));
118
120
  for (const rel of byModule.keys()) {
119
121
  for (const root of roots) {
120
122
  const target = `${root}/${rel}`;
@@ -149,10 +151,10 @@ function envContainmentPlugin(options) {
149
151
  if (source.startsWith("\0")) return null;
150
152
  const cleaned = stripQuery(source);
151
153
  let direct = null;
152
- if (path6.isAbsolute(cleaned)) {
154
+ if (path7.isAbsolute(cleaned)) {
153
155
  direct = cleaned;
154
156
  } else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
155
- direct = path6.resolve(path6.dirname(stripQuery(importer)), cleaned);
157
+ direct = path7.resolve(path7.dirname(stripQuery(importer)), cleaned);
156
158
  }
157
159
  if (direct) {
158
160
  const rel = matchTarget(direct);
@@ -161,7 +163,7 @@ function envContainmentPlugin(options) {
161
163
  }
162
164
  if (!specifierMightMatch(cleaned)) return null;
163
165
  if (typeof this.resolve !== "function") return null;
164
- const importerDir = importer ? path6.dirname(stripQuery(importer)) : "";
166
+ const importerDir = importer ? path7.dirname(stripQuery(importer)) : "";
165
167
  const outcomeKey = `${importerDir}|${source}`;
166
168
  const memoized = resolveOutcomes.get(outcomeKey);
167
169
  if (memoized !== void 0) return memoized;
@@ -264,9 +266,9 @@ function fastRefreshTolerantExportsPlugin() {
264
266
  }
265
267
  };
266
268
  }
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");
269
+ var CACHE_DIR = path7.join(process.cwd(), ".storybook-cache");
270
+ var SCREENSHOTS_DIR = path7.join(CACHE_DIR, "screenshots");
271
+ var MANIFEST_PATH = path7.join(CACHE_DIR, "manifest.json");
270
272
  var VIEWPORT_WIDTH = 1920;
271
273
  var VIEWPORT_HEIGHT = 1080;
272
274
  var MIN_COMPONENT_WIDTH = 420;
@@ -274,30 +276,30 @@ var MIN_COMPONENT_HEIGHT = 280;
274
276
 
275
277
  // src/utils/fileSystem/fileSystem.ts
276
278
  function ensureCacheDirectories() {
277
- if (!fs4.existsSync(CACHE_DIR)) {
278
- fs4.mkdirSync(CACHE_DIR, { recursive: true });
279
+ if (!fs5.existsSync(CACHE_DIR)) {
280
+ fs5.mkdirSync(CACHE_DIR, { recursive: true });
279
281
  }
280
- if (!fs4.existsSync(SCREENSHOTS_DIR)) {
281
- fs4.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
282
+ if (!fs5.existsSync(SCREENSHOTS_DIR)) {
283
+ fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
282
284
  }
283
285
  }
284
286
  function computeFileHash(filePath) {
285
- if (!fs4.existsSync(filePath)) {
287
+ if (!fs5.existsSync(filePath)) {
286
288
  return "";
287
289
  }
288
- const content = fs4.readFileSync(filePath, "utf-8");
290
+ const content = fs5.readFileSync(filePath, "utf-8");
289
291
  return crypto.createHash("sha256").update(content).digest("hex");
290
292
  }
291
293
  function loadManifest() {
292
- if (fs4.existsSync(MANIFEST_PATH)) {
293
- const content = fs4.readFileSync(MANIFEST_PATH, "utf-8");
294
+ if (fs5.existsSync(MANIFEST_PATH)) {
295
+ const content = fs5.readFileSync(MANIFEST_PATH, "utf-8");
294
296
  return JSON.parse(content);
295
297
  }
296
298
  return { stories: {} };
297
299
  }
298
300
  function saveManifest(manifest) {
299
301
  ensureCacheDirectories();
300
- fs4.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
302
+ fs5.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
301
303
  }
302
304
  function updateManifest(storyId, sourcePath, fileHash, boundingBox) {
303
305
  const manifest = loadManifest();
@@ -386,11 +388,76 @@ async function getBrowser() {
386
388
  }
387
389
  return browserPromise;
388
390
  }
391
+ function classifyPreviewSnapshot(s) {
392
+ if (s.text.includes("Sorry, something went wrong")) return "errored";
393
+ if (s.text.includes("Couldn't find story")) return "errored";
394
+ if (s.hasContainedCard) return "contained";
395
+ if (s.renderStateAttr === "contained") return "contained";
396
+ if (s.renderStateAttr === "rendered") return "rendered";
397
+ if (s.text.length > 0) return "rendered";
398
+ if (s.hasSizedElement) return "rendered";
399
+ return "pending";
400
+ }
401
+ async function detectRenderState(page, opts = {}) {
402
+ const pollMs = opts.pollMs ?? 250;
403
+ const settleMs = opts.settleMs ?? 4e3;
404
+ const blankStablePolls = opts.blankStablePolls ?? 6;
405
+ const hardCapMs = opts.hardCapMs ?? 2e4;
406
+ const now = opts.now ?? Date.now;
407
+ const start = now();
408
+ let emptyStreak = 0;
409
+ const snapshot = () => page.evaluate(
410
+ ({ stateAttr, cardAttr }) => {
411
+ const root = document.getElementById("storybook-root") ?? document.body;
412
+ const text = (root?.textContent ?? "").trim();
413
+ let hasSizedElement = false;
414
+ if (root) {
415
+ const all = root.querySelectorAll("*");
416
+ for (let i = 0; i < all.length; i++) {
417
+ const rect = all[i]?.getBoundingClientRect();
418
+ if (rect && rect.width > 0 && rect.height > 0) {
419
+ hasSizedElement = true;
420
+ break;
421
+ }
422
+ }
423
+ }
424
+ return {
425
+ text,
426
+ hasContainedCard: Boolean(root?.querySelector(`[${cardAttr}]`)),
427
+ renderStateAttr: root?.getAttribute(stateAttr) ?? null,
428
+ hasSizedElement
429
+ };
430
+ },
431
+ { stateAttr: ONLOOK_RENDER_STATE_ATTR, cardAttr: ONLOOK_CONTAINED_CARD_ATTR }
432
+ );
433
+ while (true) {
434
+ let state;
435
+ let threw = false;
436
+ try {
437
+ state = classifyPreviewSnapshot(await snapshot());
438
+ } catch {
439
+ state = "pending";
440
+ threw = true;
441
+ }
442
+ if (state === "rendered" || state === "contained" || state === "errored") {
443
+ return state;
444
+ }
445
+ const elapsed = now() - start;
446
+ if (threw) {
447
+ emptyStreak = 0;
448
+ } else if (elapsed >= settleMs) {
449
+ emptyStreak += 1;
450
+ if (emptyStreak >= blankStablePolls) return "blank";
451
+ }
452
+ if (elapsed >= hardCapMs) return "timedOut";
453
+ await page.waitForTimeout(pollMs);
454
+ }
455
+ }
389
456
  function getScreenshotPath(storyId, theme) {
390
- const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
391
- return path6.join(storyDir, `${theme}.png`);
457
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
458
+ return path7.join(storyDir, `${theme}.png`);
392
459
  }
393
- async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
460
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", loadTimeoutMs = 9e4, renderTimeoutMs = 2e4) {
394
461
  const browser = await getBrowser();
395
462
  const context = await browser.newContext({
396
463
  viewport: { width, height },
@@ -399,13 +466,14 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
399
466
  const page = await context.newPage();
400
467
  try {
401
468
  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 });
469
+ await page.goto(url, { timeout: loadTimeoutMs });
470
+ await page.waitForLoadState("domcontentloaded", { timeout: loadTimeoutMs });
471
+ await page.waitForLoadState("load", { timeout: loadTimeoutMs });
405
472
  try {
406
473
  await page.waitForLoadState("networkidle", { timeout: 5e3 });
407
474
  } catch {
408
475
  }
476
+ const renderState = await detectRenderState(page, { hardCapMs: renderTimeoutMs });
409
477
  await page.evaluate(() => document.fonts.ready);
410
478
  try {
411
479
  await page.evaluate(async () => {
@@ -467,7 +535,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
467
535
  } else {
468
536
  screenshotBuffer = await page.screenshot({ type: "png" });
469
537
  }
470
- return { buffer: screenshotBuffer, boundingBox: resultBoundingBox };
538
+ return { buffer: screenshotBuffer, boundingBox: resultBoundingBox, renderState };
471
539
  } finally {
472
540
  await context.close();
473
541
  }
@@ -475,9 +543,9 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
475
543
  async function generateScreenshot(storyId, theme, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
476
544
  try {
477
545
  ensureCacheDirectories();
478
- const storyDir = path6.join(SCREENSHOTS_DIR, storyId);
479
- if (!fs4.existsSync(storyDir)) {
480
- fs4.mkdirSync(storyDir, { recursive: true });
546
+ const storyDir = path7.join(SCREENSHOTS_DIR, storyId);
547
+ if (!fs5.existsSync(storyDir)) {
548
+ fs5.mkdirSync(storyDir, { recursive: true });
481
549
  }
482
550
  const screenshotPath = getScreenshotPath(storyId, theme);
483
551
  const { buffer, boundingBox } = await captureScreenshotBuffer(
@@ -488,7 +556,7 @@ async function generateScreenshot(storyId, theme, storybookUrl = "http://localho
488
556
  storybookUrl,
489
557
  timeoutMs
490
558
  );
491
- fs4.writeFileSync(screenshotPath, buffer);
559
+ fs5.writeFileSync(screenshotPath, buffer);
492
560
  return { path: screenshotPath, boundingBox };
493
561
  } catch (error) {
494
562
  console.error(`Error generating screenshot for ${storyId} (${theme}):`, error);
@@ -596,7 +664,7 @@ async function fetchStorybookIndex() {
596
664
  }
597
665
  function getStoriesForFile(filePath) {
598
666
  if (!cachedIndex) return [];
599
- const fileName = path6.basename(filePath);
667
+ const fileName = path7.basename(filePath);
600
668
  return Object.values(cachedIndex.entries).filter((entry) => entry.type === "story" && entry.importPath.endsWith(fileName)).map((entry) => entry.id);
601
669
  }
602
670
  async function regenerateScreenshotsForFiles(files) {
@@ -788,6 +856,7 @@ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http
788
856
  const page = await context.newPage();
789
857
  const logs = [];
790
858
  const pageErrors = [];
859
+ const failedRequests = [];
791
860
  page.on("console", (msg) => {
792
861
  const level = msg.type();
793
862
  logs.push({
@@ -799,6 +868,28 @@ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http
799
868
  page.on("pageerror", (err) => {
800
869
  pageErrors.push(err.message);
801
870
  });
871
+ page.on("response", (res) => {
872
+ const status = res.status();
873
+ if (status >= 400) {
874
+ const req = res.request();
875
+ failedRequests.push({
876
+ url: res.url(),
877
+ status,
878
+ method: req.method(),
879
+ resourceType: req.resourceType(),
880
+ failure: null
881
+ });
882
+ }
883
+ });
884
+ page.on("requestfailed", (req) => {
885
+ failedRequests.push({
886
+ url: req.url(),
887
+ status: null,
888
+ method: req.method(),
889
+ resourceType: req.resourceType(),
890
+ failure: req.failure()?.errorText ?? "request failed"
891
+ });
892
+ });
802
893
  const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
803
894
  try {
804
895
  await page.goto(url, { timeout: timeoutMs });
@@ -809,7 +900,7 @@ async function captureConsoleLogs(storyId, theme = "light", storybookUrl = "http
809
900
  } catch {
810
901
  }
811
902
  await new Promise((r) => setTimeout(r, 1e3));
812
- return { logs, pageErrors, url };
903
+ return { logs, pageErrors, failedRequests, url };
813
904
  } finally {
814
905
  await context.close();
815
906
  }
@@ -884,13 +975,115 @@ function softStoryRerenderPlugin() {
884
975
  preview.onStoriesChanged({ importFn: newModule.importFn });
885
976
  }
886
977
  });`;
887
- const out = code.replace(pattern, (_match, quote, path7) => {
888
- return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path7}${quote}`);
978
+ const out = code.replace(pattern, (_match, quote, path8) => {
979
+ return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${quote}${path8}${quote}`);
889
980
  });
890
981
  return out === code ? null : { code: out, map: null };
891
982
  }
892
983
  };
893
984
  }
985
+ var TSCONFIG_CANDIDATES = ["tsconfig.json", "tsconfig.app.json", "tsconfig.base.json"];
986
+ function resolveTsconfigAliases(root) {
987
+ try {
988
+ const config = readTsconfigAliasConfig(root);
989
+ if (!config) return [];
990
+ const base = path7.resolve(root, config.baseUrl ?? ".");
991
+ const aliases = [];
992
+ for (const [key, targets] of Object.entries(config.paths)) {
993
+ const target = targets[0];
994
+ if (!target) continue;
995
+ if (key.endsWith("/*") && target.endsWith("/*")) {
996
+ const findPrefix = key.slice(0, -1);
997
+ const targetDir = path7.resolve(base, target.slice(0, -2));
998
+ aliases.push({
999
+ find: new RegExp(`^${escapeRegExp(findPrefix)}`),
1000
+ replacement: `${targetDir}/`
1001
+ });
1002
+ } else if (!key.includes("*") && !target.includes("*")) {
1003
+ aliases.push({
1004
+ find: new RegExp(`^${escapeRegExp(key)}$`),
1005
+ replacement: path7.resolve(base, target)
1006
+ });
1007
+ }
1008
+ }
1009
+ return aliases.sort((a, b) => b.find.source.length - a.find.source.length);
1010
+ } catch {
1011
+ return [];
1012
+ }
1013
+ }
1014
+ function readTsconfigAliasConfig(root) {
1015
+ for (const candidate of TSCONFIG_CANDIDATES) {
1016
+ const abs = path7.join(root, candidate);
1017
+ let text;
1018
+ try {
1019
+ text = fs5.readFileSync(abs, "utf-8");
1020
+ } catch {
1021
+ continue;
1022
+ }
1023
+ let parsed;
1024
+ try {
1025
+ parsed = JSON.parse(stripJsonComments(text));
1026
+ } catch {
1027
+ continue;
1028
+ }
1029
+ const paths = parsed.compilerOptions?.paths;
1030
+ if (paths && typeof paths === "object" && Object.keys(paths).length > 0) {
1031
+ const baseUrl = parsed.compilerOptions?.baseUrl;
1032
+ return typeof baseUrl === "string" ? { baseUrl, paths } : { paths };
1033
+ }
1034
+ }
1035
+ return null;
1036
+ }
1037
+ function escapeRegExp(value) {
1038
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1039
+ }
1040
+ function stripJsonComments(text) {
1041
+ let out = "";
1042
+ let inString = false;
1043
+ let inLine = false;
1044
+ let inBlock = false;
1045
+ for (let i = 0; i < text.length; i++) {
1046
+ const c = text.charAt(i);
1047
+ const next = text.charAt(i + 1);
1048
+ if (inLine) {
1049
+ if (c === "\n") {
1050
+ inLine = false;
1051
+ out += c;
1052
+ }
1053
+ continue;
1054
+ }
1055
+ if (inBlock) {
1056
+ if (c === "*" && next === "/") {
1057
+ inBlock = false;
1058
+ i++;
1059
+ }
1060
+ continue;
1061
+ }
1062
+ if (inString) {
1063
+ out += c;
1064
+ if (c === "\\") {
1065
+ out += next;
1066
+ i++;
1067
+ } else if (c === '"') {
1068
+ inString = false;
1069
+ }
1070
+ continue;
1071
+ }
1072
+ if (c === '"') {
1073
+ inString = true;
1074
+ out += c;
1075
+ } else if (c === "/" && next === "/") {
1076
+ inLine = true;
1077
+ i++;
1078
+ } else if (c === "/" && next === "*") {
1079
+ inBlock = true;
1080
+ i++;
1081
+ } else {
1082
+ out += c;
1083
+ }
1084
+ }
1085
+ return out;
1086
+ }
894
1087
  function findGitRoot(startPath) {
895
1088
  let currentPath = startPath;
896
1089
  while (currentPath !== dirname(currentPath)) {
@@ -917,8 +1110,8 @@ var manifestCache = null;
917
1110
  var manifestHash = null;
918
1111
  function readManifestFromDisk(filePath) {
919
1112
  try {
920
- if (!fs4.existsSync(filePath)) return null;
921
- const raw = fs4.readFileSync(filePath, "utf-8");
1113
+ if (!fs5.existsSync(filePath)) return null;
1114
+ const raw = fs5.readFileSync(filePath, "utf-8");
922
1115
  const hash = createHash("sha1").update(raw).digest("hex");
923
1116
  const manifest = JSON.parse(raw);
924
1117
  return { manifest, hash };
@@ -961,10 +1154,10 @@ var TAILWIND_ENTRY_CANDIDATES = [
961
1154
  var TAILWIND_DIRECTIVE_PATTERN = /@import\s+['"](?:tailwindcss|tailwindcss\/.+)['"]|@tailwind\s+(?:base|components|utilities)/;
962
1155
  function detectTailwindEntryCss(cwd) {
963
1156
  for (const rel of TAILWIND_ENTRY_CANDIDATES) {
964
- const abs = path6.join(cwd, rel);
1157
+ const abs = path7.join(cwd, rel);
965
1158
  try {
966
- if (!fs4.existsSync(abs)) continue;
967
- const head = fs4.readFileSync(abs, "utf-8").slice(0, 2048);
1159
+ if (!fs5.existsSync(abs)) continue;
1160
+ const head = fs5.readFileSync(abs, "utf-8").slice(0, 2048);
968
1161
  if (TAILWIND_DIRECTIVE_PATTERN.test(head)) return abs;
969
1162
  } catch {
970
1163
  }
@@ -1039,9 +1232,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1039
1232
  res.end(JSON.stringify({ error: 'theme must be "light" or "dark"' }));
1040
1233
  return;
1041
1234
  }
1042
- captureScreenshotBuffer(storyId, theme, width, height).then(({ buffer }) => {
1235
+ captureScreenshotBuffer(storyId, theme, width, height).then(({ buffer, renderState }) => {
1043
1236
  res.setHeader("Content-Type", "image/png");
1044
1237
  res.setHeader("Access-Control-Allow-Origin", "*");
1238
+ res.setHeader("Access-Control-Expose-Headers", "x-onlook-render-state");
1239
+ res.setHeader("x-onlook-render-state", renderState);
1045
1240
  res.setHeader("Cache-Control", "no-cache");
1046
1241
  res.end(buffer);
1047
1242
  }).catch((error) => {
@@ -1092,7 +1287,7 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1092
1287
  return;
1093
1288
  }
1094
1289
  if (req.url?.startsWith("/screenshots/")) {
1095
- const screenshotPath = path6.join(
1290
+ const screenshotPath = path7.join(
1096
1291
  process.cwd(),
1097
1292
  ".storybook-cache",
1098
1293
  req.url.replace("/screenshots/", "screenshots/")
@@ -1101,11 +1296,11 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1101
1296
  const storyId = urlParts[0];
1102
1297
  const themeFile = urlParts[1];
1103
1298
  const theme = themeFile?.replace(".png", "");
1104
- if (fs4.existsSync(screenshotPath)) {
1299
+ if (fs5.existsSync(screenshotPath)) {
1105
1300
  res.setHeader("Content-Type", "image/png");
1106
1301
  res.setHeader("Access-Control-Allow-Origin", "*");
1107
1302
  res.setHeader("Cache-Control", "public, max-age=3600");
1108
- fs4.createReadStream(screenshotPath).pipe(res);
1303
+ fs5.createReadStream(screenshotPath).pipe(res);
1109
1304
  return;
1110
1305
  }
1111
1306
  if (storyId && theme && (theme === "light" || theme === "dark")) {
@@ -1113,16 +1308,16 @@ var serveMetadataAndScreenshots = (req, res, next) => {
1113
1308
  `[STORYBOOK_PLUGIN] Generating screenshot on-demand: ${storyId}/${theme}`
1114
1309
  );
1115
1310
  captureScreenshotBuffer(storyId, theme).then(({ buffer }) => {
1116
- const storyDir = path6.join(
1311
+ const storyDir = path7.join(
1117
1312
  process.cwd(),
1118
1313
  ".storybook-cache",
1119
1314
  "screenshots",
1120
1315
  storyId
1121
1316
  );
1122
- if (!fs4.existsSync(storyDir)) {
1123
- fs4.mkdirSync(storyDir, { recursive: true });
1317
+ if (!fs5.existsSync(storyDir)) {
1318
+ fs5.mkdirSync(storyDir, { recursive: true });
1124
1319
  }
1125
- fs4.writeFileSync(screenshotPath, buffer);
1320
+ fs5.writeFileSync(screenshotPath, buffer);
1126
1321
  res.setHeader("Content-Type", "image/png");
1127
1322
  res.setHeader("Access-Control-Allow-Origin", "*");
1128
1323
  res.setHeader("Cache-Control", "public, max-age=3600");
@@ -1196,10 +1391,20 @@ function storybookOnlookPlugin(options = {}) {
1196
1391
  repoRoot,
1197
1392
  hmr: hmrOverride ?? null
1198
1393
  });
1394
+ const aliasEntries = options.resolveAliases === false ? [] : resolveTsconfigAliases(process.cwd());
1395
+ if (aliasEntries.length > 0) {
1396
+ console.log("[STORYBOOK_PLUGIN] Resolved tsconfig path aliases", {
1397
+ count: aliasEntries.length,
1398
+ aliases: aliasEntries.map((a) => a.find.source)
1399
+ });
1400
+ }
1199
1401
  const mainPlugin = {
1200
1402
  name: "storybook-onlook-plugin",
1201
1403
  config() {
1202
1404
  return {
1405
+ ...aliasEntries.length > 0 && {
1406
+ resolve: { alias: aliasEntries }
1407
+ },
1203
1408
  server: {
1204
1409
  // HMR override applies only when the caller explicitly opts in
1205
1410
  // (typically: Storybook fronted over HTTPS by a reverse proxy).
@@ -1220,7 +1425,7 @@ function storybookOnlookPlugin(options = {}) {
1220
1425
  configureServer(server) {
1221
1426
  console.log("[STORYBOOK_PLUGIN] Configuring server middleware");
1222
1427
  server.middlewares.use(serveMetadataAndScreenshots);
1223
- const cachedManifestPath = path6.join(
1428
+ const cachedManifestPath = path7.join(
1224
1429
  process.cwd(),
1225
1430
  ".storybook-cache",
1226
1431
  "manifest.json"
@@ -1239,7 +1444,7 @@ function storybookOnlookPlugin(options = {}) {
1239
1444
  server.watcher.on("change", scheduleRefresh);
1240
1445
  const storyGlobs = options.storyGlobs ?? ["**/*.stories.@(js|jsx|mjs|ts|tsx|mdx)"];
1241
1446
  const resolvedStoryGlobs = storyGlobs.map(
1242
- (glob) => path6.isAbsolute(glob) ? glob : path6.join(process.cwd(), glob)
1447
+ (glob) => path7.isAbsolute(glob) ? glob : path7.join(process.cwd(), glob)
1243
1448
  );
1244
1449
  if (resolvedStoryGlobs.length > 0) {
1245
1450
  server.watcher.add(resolvedStoryGlobs);
@@ -1250,7 +1455,7 @@ function storybookOnlookPlugin(options = {}) {
1250
1455
  const tailwindEntryPath = (() => {
1251
1456
  if (options.tailwindEntryCss === false) return null;
1252
1457
  if (typeof options.tailwindEntryCss === "string") {
1253
- return path6.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path6.join(process.cwd(), options.tailwindEntryCss);
1458
+ return path7.isAbsolute(options.tailwindEntryCss) ? options.tailwindEntryCss : path7.join(process.cwd(), options.tailwindEntryCss);
1254
1459
  }
1255
1460
  return detectTailwindEntryCss(process.cwd());
1256
1461
  })();
@@ -1266,13 +1471,13 @@ function storybookOnlookPlugin(options = {}) {
1266
1471
  });
1267
1472
  server.watcher.on("add", (file) => {
1268
1473
  if (!/\.tsx?$/.test(file)) return;
1269
- const dir = path6.dirname(file);
1474
+ const dir = path7.dirname(file);
1270
1475
  const isNewDir = !seenDirs.has(dir);
1271
1476
  seenDirs.add(dir);
1272
1477
  if (!watcherReady || !isNewDir) return;
1273
1478
  try {
1274
1479
  const now = /* @__PURE__ */ new Date();
1275
- fs4.utimesSync(tailwindEntryPath, now, now);
1480
+ fs5.utimesSync(tailwindEntryPath, now, now);
1276
1481
  console.log("[STORYBOOK_PLUGIN] Tailwind rescan for new dir", {
1277
1482
  dir,
1278
1483
  entry: tailwindEntryPath
@@ -1297,7 +1502,7 @@ function storybookOnlookPlugin(options = {}) {
1297
1502
  configurePreviewServer(server) {
1298
1503
  console.log("[STORYBOOK_PLUGIN] Configuring preview server middleware");
1299
1504
  server.middlewares.use(serveMetadataAndScreenshots);
1300
- refreshManifest(path6.join(process.cwd(), ".storybook-cache", "manifest.json"));
1505
+ refreshManifest(path7.join(process.cwd(), ".storybook-cache", "manifest.json"));
1301
1506
  },
1302
1507
  handleHotUpdate: handleStoryFileChange
1303
1508
  };
@@ -1324,7 +1529,8 @@ var OPTION_KEY_MAP = {
1324
1529
  storyGlobs: true,
1325
1530
  tailwindEntryCss: true,
1326
1531
  envContainment: true,
1327
- moduleLoadCatchAll: true
1532
+ moduleLoadCatchAll: true,
1533
+ resolveAliases: true
1328
1534
  };
1329
1535
  var OPTION_KEYS = Object.keys(OPTION_KEY_MAP);
1330
1536
  function pluginOptionsFromPresetOptions(options) {
@@ -141,6 +141,77 @@ async function closeBrowser() {
141
141
  console.error("Error closing browser:", error);
142
142
  }
143
143
  }
144
+
145
+ // src/containment-contract/containment-contract.ts
146
+ var ONLOOK_RENDER_STATE_ATTR = "data-onlook-render-state";
147
+ var ONLOOK_CONTAINED_CARD_ATTR = "data-onlook-contained-card";
148
+
149
+ // src/screenshot-service/utils/screenshot/screenshot.ts
150
+ function classifyPreviewSnapshot(s) {
151
+ if (s.text.includes("Sorry, something went wrong")) return "errored";
152
+ if (s.text.includes("Couldn't find story")) return "errored";
153
+ if (s.hasContainedCard) return "contained";
154
+ if (s.renderStateAttr === "contained") return "contained";
155
+ if (s.renderStateAttr === "rendered") return "rendered";
156
+ if (s.text.length > 0) return "rendered";
157
+ if (s.hasSizedElement) return "rendered";
158
+ return "pending";
159
+ }
160
+ async function detectRenderState(page, opts = {}) {
161
+ const pollMs = opts.pollMs ?? 250;
162
+ const settleMs = opts.settleMs ?? 4e3;
163
+ const blankStablePolls = opts.blankStablePolls ?? 6;
164
+ const hardCapMs = opts.hardCapMs ?? 2e4;
165
+ const now = opts.now ?? Date.now;
166
+ const start = now();
167
+ let emptyStreak = 0;
168
+ const snapshot = () => page.evaluate(
169
+ ({ stateAttr, cardAttr }) => {
170
+ const root = document.getElementById("storybook-root") ?? document.body;
171
+ const text = (root?.textContent ?? "").trim();
172
+ let hasSizedElement = false;
173
+ if (root) {
174
+ const all = root.querySelectorAll("*");
175
+ for (let i = 0; i < all.length; i++) {
176
+ const rect = all[i]?.getBoundingClientRect();
177
+ if (rect && rect.width > 0 && rect.height > 0) {
178
+ hasSizedElement = true;
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ return {
184
+ text,
185
+ hasContainedCard: Boolean(root?.querySelector(`[${cardAttr}]`)),
186
+ renderStateAttr: root?.getAttribute(stateAttr) ?? null,
187
+ hasSizedElement
188
+ };
189
+ },
190
+ { stateAttr: ONLOOK_RENDER_STATE_ATTR, cardAttr: ONLOOK_CONTAINED_CARD_ATTR }
191
+ );
192
+ while (true) {
193
+ let state;
194
+ let threw = false;
195
+ try {
196
+ state = classifyPreviewSnapshot(await snapshot());
197
+ } catch {
198
+ state = "pending";
199
+ threw = true;
200
+ }
201
+ if (state === "rendered" || state === "contained" || state === "errored") {
202
+ return state;
203
+ }
204
+ const elapsed = now() - start;
205
+ if (threw) {
206
+ emptyStreak = 0;
207
+ } else if (elapsed >= settleMs) {
208
+ emptyStreak += 1;
209
+ if (emptyStreak >= blankStablePolls) return "blank";
210
+ }
211
+ if (elapsed >= hardCapMs) return "timedOut";
212
+ await page.waitForTimeout(pollMs);
213
+ }
214
+ }
144
215
  function getScreenshotPath(storyId, theme) {
145
216
  const storyDir = path.join(SCREENSHOTS_DIR, storyId);
146
217
  return path.join(storyDir, `${theme}.png`);
@@ -149,7 +220,7 @@ function screenshotExists(storyId, theme) {
149
220
  const screenshotPath = getScreenshotPath(storyId, theme);
150
221
  return fs.existsSync(screenshotPath);
151
222
  }
152
- async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", timeoutMs = 3e4) {
223
+ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, height = VIEWPORT_HEIGHT, storybookUrl = "http://localhost:6006", loadTimeoutMs = 9e4, renderTimeoutMs = 2e4) {
153
224
  const browser = await getBrowser();
154
225
  const context = await browser.newContext({
155
226
  viewport: { width, height },
@@ -158,13 +229,14 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
158
229
  const page = await context.newPage();
159
230
  try {
160
231
  const url = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story&globals=theme:${theme}`;
161
- await page.goto(url, { timeout: timeoutMs });
162
- await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs });
163
- await page.waitForLoadState("load", { timeout: timeoutMs });
232
+ await page.goto(url, { timeout: loadTimeoutMs });
233
+ await page.waitForLoadState("domcontentloaded", { timeout: loadTimeoutMs });
234
+ await page.waitForLoadState("load", { timeout: loadTimeoutMs });
164
235
  try {
165
236
  await page.waitForLoadState("networkidle", { timeout: 5e3 });
166
237
  } catch {
167
238
  }
239
+ const renderState = await detectRenderState(page, { hardCapMs: renderTimeoutMs });
168
240
  await page.evaluate(() => document.fonts.ready);
169
241
  try {
170
242
  await page.evaluate(async () => {
@@ -226,7 +298,7 @@ async function captureScreenshotBuffer(storyId, theme, width = VIEWPORT_WIDTH, h
226
298
  } else {
227
299
  screenshotBuffer = await page.screenshot({ type: "png" });
228
300
  }
229
- return { buffer: screenshotBuffer, boundingBox: resultBoundingBox };
301
+ return { buffer: screenshotBuffer, boundingBox: resultBoundingBox, renderState };
230
302
  } finally {
231
303
  await context.close();
232
304
  }