@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.
- package/dist/cli/index.js +119 -29
- package/dist/index.d.ts +1 -1
- package/dist/index.js +256 -53
- package/dist/preset/index.d.ts +1 -1
- package/dist/preset/index.js +260 -54
- package/dist/screenshot-service/index.js +77 -5
- package/dist/{storybook-onlook-plugin-CigILdDb.d.ts → storybook-onlook-plugin-B9Eo_OIq.d.ts} +1 -0
- package/package.json +1 -1
package/dist/preset/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import crypto, { createHash } from 'crypto';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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 =
|
|
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(
|
|
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 (
|
|
154
|
+
if (path7.isAbsolute(cleaned)) {
|
|
153
155
|
direct = cleaned;
|
|
154
156
|
} else if (importer && (cleaned.startsWith("./") || cleaned.startsWith("../"))) {
|
|
155
|
-
direct =
|
|
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 ?
|
|
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 =
|
|
268
|
-
var SCREENSHOTS_DIR =
|
|
269
|
-
var MANIFEST_PATH =
|
|
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 (!
|
|
278
|
-
|
|
279
|
+
if (!fs5.existsSync(CACHE_DIR)) {
|
|
280
|
+
fs5.mkdirSync(CACHE_DIR, { recursive: true });
|
|
279
281
|
}
|
|
280
|
-
if (!
|
|
281
|
-
|
|
282
|
+
if (!fs5.existsSync(SCREENSHOTS_DIR)) {
|
|
283
|
+
fs5.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
282
284
|
}
|
|
283
285
|
}
|
|
284
286
|
function computeFileHash(filePath) {
|
|
285
|
-
if (!
|
|
287
|
+
if (!fs5.existsSync(filePath)) {
|
|
286
288
|
return "";
|
|
287
289
|
}
|
|
288
|
-
const content =
|
|
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 (
|
|
293
|
-
const content =
|
|
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
|
-
|
|
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 =
|
|
391
|
-
return
|
|
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",
|
|
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:
|
|
403
|
-
await page.waitForLoadState("domcontentloaded", { timeout:
|
|
404
|
-
await page.waitForLoadState("load", { timeout:
|
|
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 =
|
|
479
|
-
if (!
|
|
480
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
888
|
-
return replacement.replace("$QUOTE$$PATH$$QUOTE$", `${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 (!
|
|
921
|
-
const raw =
|
|
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 =
|
|
1157
|
+
const abs = path7.join(cwd, rel);
|
|
965
1158
|
try {
|
|
966
|
-
if (!
|
|
967
|
-
const head =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
1311
|
+
const storyDir = path7.join(
|
|
1117
1312
|
process.cwd(),
|
|
1118
1313
|
".storybook-cache",
|
|
1119
1314
|
"screenshots",
|
|
1120
1315
|
storyId
|
|
1121
1316
|
);
|
|
1122
|
-
if (!
|
|
1123
|
-
|
|
1317
|
+
if (!fs5.existsSync(storyDir)) {
|
|
1318
|
+
fs5.mkdirSync(storyDir, { recursive: true });
|
|
1124
1319
|
}
|
|
1125
|
-
|
|
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 =
|
|
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) =>
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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",
|
|
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:
|
|
162
|
-
await page.waitForLoadState("domcontentloaded", { timeout:
|
|
163
|
-
await page.waitForLoadState("load", { timeout:
|
|
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
|
}
|