@weasel-js/forge 1.11.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -1
- package/dist/{FrameController-BRhShqut.d.ts → FrameController-CdnaIo4C.d.ts} +18 -10
- package/dist/{chunk-7BQPHMA2.js → chunk-3BAM6M26.js} +2 -2
- package/dist/chunk-3BAM6M26.js.map +1 -0
- package/dist/{chunk-ZAWXMUGZ.js → chunk-5WS67HMU.js} +8 -3
- package/dist/chunk-5WS67HMU.js.map +1 -0
- package/dist/{chunk-R5F44ECM.js → chunk-ATI7FYJ7.js} +18 -14
- package/dist/chunk-ATI7FYJ7.js.map +1 -0
- package/dist/{chunk-2OJAV522.js → chunk-GH5RE5X5.js} +2 -2
- package/dist/chunk-GH5RE5X5.js.map +1 -0
- package/dist/{chunk-SQ4KCIEH.js → chunk-KEQBGDUZ.js} +3 -3
- package/dist/{chunk-SQ4KCIEH.js.map → chunk-KEQBGDUZ.js.map} +1 -1
- package/dist/{chunk-DPTECNYC.js → chunk-LTTEILXY.js} +3 -3
- package/dist/{chunk-DPTECNYC.js.map → chunk-LTTEILXY.js.map} +1 -1
- package/dist/chunk-ORMMZ5PV.js +59 -0
- package/dist/chunk-ORMMZ5PV.js.map +1 -0
- package/dist/{chunk-NVNTQ4AT.js → chunk-WMGEO5M6.js} +133 -9
- package/dist/chunk-WMGEO5M6.js.map +1 -0
- package/dist/chunk-YDLRSEUN.js +501 -0
- package/dist/chunk-YDLRSEUN.js.map +1 -0
- package/dist/{config-BwScQukB.d.ts → config-Mg05Ja9r.d.ts} +20 -3
- package/dist/csf/preview-api.js +2 -2
- package/dist/frame/index.css +12 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +62 -29
- package/dist/frame/index.js +142 -15
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
- package/dist/shell/index.css +131 -6
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +44 -16
- package/dist/shell/index.js +1663 -280
- package/dist/shell/index.js.map +1 -1
- package/dist/test/index.css +58 -0
- package/dist/test/index.css.map +1 -0
- package/dist/test/index.d.ts +2 -4
- package/dist/test/index.js +11 -13
- package/dist/test/index.js.map +1 -1
- package/dist/vite/index.js +515 -111
- package/dist/vite/index.js.map +1 -1
- package/package.json +13 -8
- package/dist/chunk-2OJAV522.js.map +0 -1
- package/dist/chunk-5DXUNLV2.js +0 -6
- package/dist/chunk-5DXUNLV2.js.map +0 -1
- package/dist/chunk-7BQPHMA2.js.map +0 -1
- package/dist/chunk-J3GYSNCK.js +0 -285
- package/dist/chunk-J3GYSNCK.js.map +0 -1
- package/dist/chunk-NVNTQ4AT.js.map +0 -1
- package/dist/chunk-R5F44ECM.js.map +0 -1
- package/dist/chunk-ZAWXMUGZ.js.map +0 -1
package/dist/vite/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { DEFAULT_TITLE } from '../chunk-
|
|
1
|
+
import { CAPTURE_PAGES, DEFAULT_TITLE, CAPTURE_PATH, parsePayload, messageOf } from '../chunk-ORMMZ5PV.js';
|
|
2
2
|
import { isGallery } from '../chunk-WEQSLPKT.js';
|
|
3
|
-
import { PROP_ARG_TYPES_EXPORT, storyNameFromExport, storyId } from '../chunk-
|
|
3
|
+
import { PROP_ARG_TYPES_EXPORT, storyNameFromExport, storyId } from '../chunk-5WS67HMU.js';
|
|
4
4
|
import { readFileSync, mkdtempSync, existsSync, mkdirSync, writeFileSync, globSync, statSync, watch, rmSync } from 'fs';
|
|
5
5
|
import { resolve, sep, relative, join, dirname, basename, matchesGlob, posix } from 'path';
|
|
6
6
|
import { parse } from '@babel/parser';
|
|
7
7
|
import { execFile } from 'child_process';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { stableStringify } from '@weasel-js/core/json';
|
|
9
11
|
import { tmpdir } from 'os';
|
|
10
12
|
|
|
11
13
|
// src/vite/html.ts
|
|
@@ -81,7 +83,7 @@ function createDepGraph({ root, read }) {
|
|
|
81
83
|
reset() {
|
|
82
84
|
resolutions.clear();
|
|
83
85
|
},
|
|
84
|
-
async build(entries,
|
|
86
|
+
async build(entries, resolve6) {
|
|
85
87
|
const resolveFrom = (spec, importer) => {
|
|
86
88
|
let byImporter = resolutions.get(importer);
|
|
87
89
|
if (!byImporter) {
|
|
@@ -90,7 +92,7 @@ function createDepGraph({ root, read }) {
|
|
|
90
92
|
}
|
|
91
93
|
let pending = byImporter.get(spec);
|
|
92
94
|
if (!pending) {
|
|
93
|
-
pending =
|
|
95
|
+
pending = resolve6(spec, importer).then(
|
|
94
96
|
(id) => {
|
|
95
97
|
const file = id?.split("?")[0];
|
|
96
98
|
return file && !file.startsWith("\0") && SCRIPT.test(file) && !file.split(/[\\/]/).includes("node_modules") ? file : null;
|
|
@@ -344,7 +346,13 @@ function fileDates(log, paths) {
|
|
|
344
346
|
const name = files.at(-1);
|
|
345
347
|
const path = name === void 0 ? void 0 : names.get(name);
|
|
346
348
|
if (!status || name === void 0 || path === void 0) continue;
|
|
347
|
-
|
|
349
|
+
const newer = out.get(path);
|
|
350
|
+
out.set(path, {
|
|
351
|
+
created: date,
|
|
352
|
+
modified: newer?.modified ?? date,
|
|
353
|
+
createdBy: commit,
|
|
354
|
+
modifiedBy: newer?.modifiedBy ?? commit
|
|
355
|
+
});
|
|
348
356
|
const renamed = status.startsWith("R") ? files[0] : void 0;
|
|
349
357
|
if (status === "A" || renamed !== void 0) names.delete(name);
|
|
350
358
|
if (renamed !== void 0) names.set(renamed, path);
|
|
@@ -384,11 +392,16 @@ function createHistoryReader(root, git = runGit) {
|
|
|
384
392
|
const history = {};
|
|
385
393
|
for (const [name, paths] of byPackage) {
|
|
386
394
|
const refs = ["v[0-9]*", "[0-9]*", ...name === null ? [] : [`${name}@[0-9]*`]].map((ref) => `--refs=${ref}`);
|
|
387
|
-
const commits = paths.
|
|
395
|
+
const commits = paths.flatMap((path) => [dates.get(path).createdBy, dates.get(path).modifiedBy]);
|
|
388
396
|
const named = (await git(["name-rev", "--tags", "--name-only", ...refs, ...commits], top)).split("\n");
|
|
389
397
|
paths.forEach((path, i) => {
|
|
390
398
|
const { created, modified } = dates.get(path);
|
|
391
|
-
history[inRepo.get(path)] = {
|
|
399
|
+
history[inRepo.get(path)] = {
|
|
400
|
+
created,
|
|
401
|
+
modified,
|
|
402
|
+
introduced: versionOf(named[2 * i] ?? ""),
|
|
403
|
+
modifiedIn: versionOf(named[2 * i + 1] ?? "")
|
|
404
|
+
};
|
|
392
405
|
});
|
|
393
406
|
}
|
|
394
407
|
return history;
|
|
@@ -842,13 +855,13 @@ function createTracer(read) {
|
|
|
842
855
|
}
|
|
843
856
|
return modules.get(file) ?? null;
|
|
844
857
|
};
|
|
845
|
-
return (
|
|
858
|
+
return (resolve6) => {
|
|
846
859
|
const trace = {
|
|
847
860
|
reads: /* @__PURE__ */ new Set(),
|
|
848
861
|
unresolved: false,
|
|
849
862
|
info,
|
|
850
863
|
async target(spec, importer) {
|
|
851
|
-
const id = (await
|
|
864
|
+
const id = (await resolve6(spec, importer).catch(() => null))?.split("?")[0];
|
|
852
865
|
if (!id) trace.unresolved = true;
|
|
853
866
|
return id && !id.startsWith("\0") ? id : null;
|
|
854
867
|
},
|
|
@@ -890,8 +903,8 @@ function sameResolution(a, b) {
|
|
|
890
903
|
}
|
|
891
904
|
function createWrapperResolver(read) {
|
|
892
905
|
const tracer = createTracer(read);
|
|
893
|
-
return async (file, callees,
|
|
894
|
-
const trace = tracer(
|
|
906
|
+
return async (file, callees, resolve6) => {
|
|
907
|
+
const trace = tracer(resolve6);
|
|
895
908
|
const wrappers = /* @__PURE__ */ new Set();
|
|
896
909
|
const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
|
|
897
910
|
if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
|
|
@@ -918,8 +931,8 @@ function indexStory(code, file, source, resolved) {
|
|
|
918
931
|
const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
|
|
919
932
|
return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
|
|
920
933
|
}
|
|
921
|
-
async function indexStoryResolved(code, file, source,
|
|
922
|
-
const resolution = await decide(file, foreignCallees(code, file),
|
|
934
|
+
async function indexStoryResolved(code, file, source, resolve6, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
|
|
935
|
+
const resolution = await decide(file, foreignCallees(code, file), resolve6);
|
|
923
936
|
return { entries: indexStory(code, file, source, resolution), resolution };
|
|
924
937
|
}
|
|
925
938
|
function ownEntries(realms) {
|
|
@@ -934,56 +947,6 @@ function ownEntries(realms) {
|
|
|
934
947
|
}
|
|
935
948
|
return [];
|
|
936
949
|
}
|
|
937
|
-
|
|
938
|
-
// src/vite/entries.ts
|
|
939
|
-
function shellEntry(base, stories) {
|
|
940
|
-
return `import { mountWorkshop } from '@weasel-js/forge/shell';
|
|
941
|
-
import '@weasel-js/labkit/styles.css';
|
|
942
|
-
import '@weasel-js/forge/shell.css';
|
|
943
|
-
import index from 'virtual:forge/index.js';
|
|
944
|
-
import importers from 'virtual:forge/importers.js';
|
|
945
|
-
import config from 'virtual:forge/shell-config.js';
|
|
946
|
-
import setup from 'virtual:forge/frame-config.js';
|
|
947
|
-
|
|
948
|
-
const workshop = mountWorkshop({
|
|
949
|
-
index,
|
|
950
|
-
importers,
|
|
951
|
-
setup,
|
|
952
|
-
config,
|
|
953
|
-
frameUrl: ${JSON.stringify(`${base}frame.html`)},
|
|
954
|
-
stories: ${JSON.stringify(stories)},
|
|
955
|
-
});
|
|
956
|
-
import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
|
|
957
|
-
// Read from source on first request, so the graph costs nothing until the page is up.
|
|
958
|
-
import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
|
|
959
|
-
import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
|
|
960
|
-
// An edit to a story file, or to a module one imports, reaches here as an update of the importers module, whose
|
|
961
|
-
// fresh import() URLs carry the new timestamps; the files it reaches arrive just before, so the reload waits for
|
|
962
|
-
// the importers that can fetch them.
|
|
963
|
-
const stale = new Set();
|
|
964
|
-
import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
|
|
965
|
-
import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
|
|
966
|
-
if (!next) return;
|
|
967
|
-
workshop.setImporters(next.default);
|
|
968
|
-
for (const file of stale) workshop.reloadStory(file);
|
|
969
|
-
stale.clear();
|
|
970
|
-
});
|
|
971
|
-
`;
|
|
972
|
-
}
|
|
973
|
-
function frameEntry(styles) {
|
|
974
|
-
return `import { mountFrame } from '@weasel-js/forge/frame';
|
|
975
|
-
import '@weasel-js/forge/frame.css';
|
|
976
|
-
${styles.map((path) => `import ${JSON.stringify(path)};
|
|
977
|
-
`).join("")}import index from 'virtual:forge/index.js';
|
|
978
|
-
import importers from 'virtual:forge/importers.js';
|
|
979
|
-
import setup from 'virtual:forge/frame-config.js';
|
|
980
|
-
|
|
981
|
-
mountFrame({ index, importers, setup });
|
|
982
|
-
// The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
|
|
983
|
-
// showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
|
|
984
|
-
import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
985
|
-
`;
|
|
986
|
-
}
|
|
987
950
|
var PLATFORM = /[\\/]node_modules[\\/](@types[\\/]react(-dom)?|typescript)[\\/]/;
|
|
988
951
|
async function createPropReader(root, stories, { skip } = {}) {
|
|
989
952
|
const skipped = (file) => PLATFORM.test(file) || skip?.(file) === true;
|
|
@@ -1148,10 +1111,339 @@ export const ${PROP_ARG_TYPES_EXPORT} = ${JSON.stringify(table.argTypes)};
|
|
|
1148
1111
|
}
|
|
1149
1112
|
};
|
|
1150
1113
|
}
|
|
1114
|
+
var IDLE_CLOSE_MS = 12e4;
|
|
1115
|
+
var SHOT_DEADLINE_MS = 2e4;
|
|
1116
|
+
var QUEUE_CAP = 8;
|
|
1117
|
+
var QUEUE_FULL = "capture queue is full";
|
|
1118
|
+
var INSTALL_PLAYWRIGHT = "install playwright";
|
|
1119
|
+
var INSTALL_CHROMIUM = "install Chromium for Playwright: npx playwright install chromium";
|
|
1120
|
+
var loadPlaywright = () => import('playwright');
|
|
1121
|
+
async function captureAvailability(load = loadPlaywright) {
|
|
1122
|
+
let playwright;
|
|
1123
|
+
try {
|
|
1124
|
+
playwright = await load();
|
|
1125
|
+
} catch {
|
|
1126
|
+
return { unavailable: INSTALL_PLAYWRIGHT };
|
|
1127
|
+
}
|
|
1128
|
+
let executable;
|
|
1129
|
+
try {
|
|
1130
|
+
executable = playwright.chromium.executablePath();
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
return { unavailable: messageOf(err) };
|
|
1133
|
+
}
|
|
1134
|
+
return existsSync(executable) ? { available: true } : { unavailable: INSTALL_CHROMIUM };
|
|
1135
|
+
}
|
|
1136
|
+
var WIRE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding"]);
|
|
1137
|
+
async function passThrough(route, fetchImpl) {
|
|
1138
|
+
const request = route.request();
|
|
1139
|
+
const method = request.method();
|
|
1140
|
+
if (method !== "GET" && method !== "HEAD") return route.abort();
|
|
1141
|
+
try {
|
|
1142
|
+
const response = await fetchImpl(request.url(), { method, headers: request.headers(), redirect: "manual" });
|
|
1143
|
+
await route.fulfill({
|
|
1144
|
+
status: response.status,
|
|
1145
|
+
headers: Object.fromEntries([...response.headers].filter(([name]) => !WIRE_HEADERS.has(name))),
|
|
1146
|
+
body: Buffer.from(await response.arrayBuffer())
|
|
1147
|
+
});
|
|
1148
|
+
} catch {
|
|
1149
|
+
await route.abort();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async function guard(page, url, allow, fetchImpl) {
|
|
1153
|
+
const own = new URL(url);
|
|
1154
|
+
const secure = own.protocol === "https:";
|
|
1155
|
+
const reach = new Set(allow);
|
|
1156
|
+
await page.route("**/*", (route) => {
|
|
1157
|
+
const origin = new URL(route.request().url()).origin;
|
|
1158
|
+
if (origin === own.origin) return route.continue();
|
|
1159
|
+
if (!reach.has(origin)) return route.abort();
|
|
1160
|
+
return secure ? passThrough(route, fetchImpl) : route.continue();
|
|
1161
|
+
});
|
|
1162
|
+
const ownSocket = `${secure ? "wss:" : "ws:"}//${own.host}`;
|
|
1163
|
+
await page.routeWebSocket(
|
|
1164
|
+
(u) => `${u.protocol}//${u.host}` !== ownSocket,
|
|
1165
|
+
(ws) => ws.close()
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
async function launchCaptureBrowser(load = loadPlaywright, fetchImpl = fetch) {
|
|
1169
|
+
let playwright;
|
|
1170
|
+
try {
|
|
1171
|
+
playwright = await load();
|
|
1172
|
+
} catch {
|
|
1173
|
+
return { unavailable: INSTALL_PLAYWRIGHT };
|
|
1174
|
+
}
|
|
1175
|
+
let chromium;
|
|
1176
|
+
try {
|
|
1177
|
+
chromium = Promise.resolve(await playwright.chromium.launch({ headless: true }));
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
return { unavailable: messageOf(err) };
|
|
1180
|
+
}
|
|
1181
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
1182
|
+
let idle;
|
|
1183
|
+
let outstanding = 0;
|
|
1184
|
+
let tail = Promise.resolve();
|
|
1185
|
+
const shut = async () => {
|
|
1186
|
+
clearTimeout(idle);
|
|
1187
|
+
const closing = chromium;
|
|
1188
|
+
chromium = null;
|
|
1189
|
+
contexts.clear();
|
|
1190
|
+
await closing?.then((b) => b.close(), () => {
|
|
1191
|
+
});
|
|
1192
|
+
};
|
|
1193
|
+
const arm = () => {
|
|
1194
|
+
clearTimeout(idle);
|
|
1195
|
+
idle = setTimeout(() => void shut(), IDLE_CLOSE_MS);
|
|
1196
|
+
idle.unref?.();
|
|
1197
|
+
};
|
|
1198
|
+
const browser = () => {
|
|
1199
|
+
if (!chromium) {
|
|
1200
|
+
const launching = playwright.chromium.launch({ headless: true });
|
|
1201
|
+
chromium = launching;
|
|
1202
|
+
launching.catch(() => {
|
|
1203
|
+
if (chromium === launching) chromium = null;
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return chromium;
|
|
1207
|
+
};
|
|
1208
|
+
const contextFor = (dpr, secure) => {
|
|
1209
|
+
const key = `${dpr}:${secure}`;
|
|
1210
|
+
let context = contexts.get(key);
|
|
1211
|
+
if (!context) {
|
|
1212
|
+
const making = browser().then(
|
|
1213
|
+
(b) => b.newContext({ deviceScaleFactor: dpr, ignoreHTTPSErrors: secure, serviceWorkers: "block" })
|
|
1214
|
+
);
|
|
1215
|
+
context = making;
|
|
1216
|
+
contexts.set(key, making);
|
|
1217
|
+
making.catch(() => {
|
|
1218
|
+
if (contexts.get(key) === making) contexts.delete(key);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
return context;
|
|
1222
|
+
};
|
|
1223
|
+
const render = async (page, payload, url, allow) => {
|
|
1224
|
+
await guard(page, url, allow, fetchImpl);
|
|
1225
|
+
await page.setViewportSize({ width: Math.ceil(payload.box.width), height: Math.ceil(payload.box.height) });
|
|
1226
|
+
await page.emulateMedia({ colorScheme: payload.colorScheme });
|
|
1227
|
+
await page.goto(url);
|
|
1228
|
+
await page.waitForFunction(() => window.__forgeCapture !== void 0);
|
|
1229
|
+
await page.evaluate((p) => window.__forgeCapture.render(p), payload);
|
|
1230
|
+
return page.locator("[data-fg-capture]").screenshot({ type: "png" });
|
|
1231
|
+
};
|
|
1232
|
+
const shoot = async (url, payload, allow) => {
|
|
1233
|
+
let page = null;
|
|
1234
|
+
let late = false;
|
|
1235
|
+
let timer;
|
|
1236
|
+
const deadline = new Promise((_, reject) => {
|
|
1237
|
+
timer = setTimeout(() => reject(new Error(`capture took longer than ${SHOT_DEADLINE_MS / 1e3} s`)), SHOT_DEADLINE_MS);
|
|
1238
|
+
});
|
|
1239
|
+
const work = (async () => {
|
|
1240
|
+
const opened = await (await contextFor(payload.dpr, url.startsWith("https:"))).newPage();
|
|
1241
|
+
page = opened;
|
|
1242
|
+
if (late) throw new Error("capture timed out");
|
|
1243
|
+
return render(opened, payload, url, allow);
|
|
1244
|
+
})();
|
|
1245
|
+
try {
|
|
1246
|
+
return await Promise.race([work, deadline]);
|
|
1247
|
+
} finally {
|
|
1248
|
+
late = true;
|
|
1249
|
+
clearTimeout(timer);
|
|
1250
|
+
await page?.close().catch(() => {
|
|
1251
|
+
});
|
|
1252
|
+
void work.catch(() => page?.close().catch(() => {
|
|
1253
|
+
}));
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
arm();
|
|
1257
|
+
return {
|
|
1258
|
+
capture(url, payload, allow = []) {
|
|
1259
|
+
if (outstanding >= QUEUE_CAP) return Promise.reject(new Error(QUEUE_FULL));
|
|
1260
|
+
outstanding += 1;
|
|
1261
|
+
clearTimeout(idle);
|
|
1262
|
+
const run = tail.then(() => shoot(url, payload, allow));
|
|
1263
|
+
tail = run.catch(() => {
|
|
1264
|
+
});
|
|
1265
|
+
return run.finally(() => {
|
|
1266
|
+
outstanding -= 1;
|
|
1267
|
+
if (outstanding === 0) arm();
|
|
1268
|
+
});
|
|
1269
|
+
},
|
|
1270
|
+
close: shut
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/vite/capture/cache.ts
|
|
1275
|
+
function createCaptureCache(limits = { entries: 64, bytes: 64 * 1024 * 1024 }) {
|
|
1276
|
+
const held = /* @__PURE__ */ new Map();
|
|
1277
|
+
let bytes = 0;
|
|
1278
|
+
const drop = (key) => {
|
|
1279
|
+
const png = held.get(key);
|
|
1280
|
+
if (!png) return;
|
|
1281
|
+
held.delete(key);
|
|
1282
|
+
bytes -= png.length;
|
|
1283
|
+
};
|
|
1284
|
+
return {
|
|
1285
|
+
get(key) {
|
|
1286
|
+
const png = held.get(key);
|
|
1287
|
+
if (!png) return void 0;
|
|
1288
|
+
held.delete(key);
|
|
1289
|
+
held.set(key, png);
|
|
1290
|
+
return png;
|
|
1291
|
+
},
|
|
1292
|
+
set(key, png) {
|
|
1293
|
+
drop(key);
|
|
1294
|
+
if (png.length > limits.bytes) return;
|
|
1295
|
+
held.set(key, png);
|
|
1296
|
+
bytes += png.length;
|
|
1297
|
+
for (const oldest of held.keys()) {
|
|
1298
|
+
if (held.size <= limits.entries && bytes <= limits.bytes) break;
|
|
1299
|
+
drop(oldest);
|
|
1300
|
+
}
|
|
1301
|
+
},
|
|
1302
|
+
clear() {
|
|
1303
|
+
held.clear();
|
|
1304
|
+
bytes = 0;
|
|
1305
|
+
},
|
|
1306
|
+
get size() {
|
|
1307
|
+
return held.size;
|
|
1308
|
+
},
|
|
1309
|
+
get bytes() {
|
|
1310
|
+
return bytes;
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/vite/capture/endpoint.ts
|
|
1316
|
+
var MAX_BODY = 16 * 1024 * 1024;
|
|
1317
|
+
var keyOf2 = (payload) => createHash("sha1").update(stableStringify(payload)).digest("hex");
|
|
1318
|
+
var urlHost = (address) => {
|
|
1319
|
+
const v4 = address.replace(/^::ffff:(?=\d+\.\d+\.\d+\.\d+$)/, "");
|
|
1320
|
+
return v4.includes(":") ? `[${v4.replace("%", "%25")}]` : v4;
|
|
1321
|
+
};
|
|
1322
|
+
var socketOrigin = (req, protocol) => `${protocol}://${urlHost(req.socket.localAddress ?? "127.0.0.1")}:${req.socket.localPort}`;
|
|
1323
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1324
|
+
function ownOrigin(origin, req, protocol, server) {
|
|
1325
|
+
let url;
|
|
1326
|
+
try {
|
|
1327
|
+
url = new URL(origin);
|
|
1328
|
+
} catch {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
if (url.origin === socketOrigin(req, protocol)) return true;
|
|
1332
|
+
if (url.protocol === `${protocol}:` && LOOPBACK.has(url.hostname) && url.port === String(req.socket.localPort)) return true;
|
|
1333
|
+
const printed = [...server.resolvedUrls?.local ?? [], ...server.resolvedUrls?.network ?? []];
|
|
1334
|
+
return printed.some((u) => new URL(u).origin === url.origin);
|
|
1335
|
+
}
|
|
1336
|
+
var json = (res, status, body2) => {
|
|
1337
|
+
res.statusCode = status;
|
|
1338
|
+
res.setHeader("Content-Type", "application/json");
|
|
1339
|
+
res.end(JSON.stringify(body2));
|
|
1340
|
+
};
|
|
1341
|
+
function readBody(req) {
|
|
1342
|
+
return new Promise((resolve6, reject) => {
|
|
1343
|
+
const chunks = [];
|
|
1344
|
+
let size = 0;
|
|
1345
|
+
req.on("data", (chunk) => {
|
|
1346
|
+
size += chunk.length;
|
|
1347
|
+
if (size <= MAX_BODY) return void chunks.push(chunk);
|
|
1348
|
+
req.pause();
|
|
1349
|
+
req.removeAllListeners("data");
|
|
1350
|
+
resolve6(null);
|
|
1351
|
+
});
|
|
1352
|
+
req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
1353
|
+
req.on("error", reject);
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function serveCapture(server, base, allowOrigins = () => []) {
|
|
1357
|
+
const cache = createCaptureCache();
|
|
1358
|
+
let launched = null;
|
|
1359
|
+
let revision = 0;
|
|
1360
|
+
const browser = () => {
|
|
1361
|
+
if (!launched) {
|
|
1362
|
+
const launching = launchCaptureBrowser();
|
|
1363
|
+
launched = launching;
|
|
1364
|
+
void launching.then((result) => {
|
|
1365
|
+
if ("unavailable" in result && launched === launching) launched = null;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return launched;
|
|
1369
|
+
};
|
|
1370
|
+
const protocol = server.config.server.https ? "https" : "http";
|
|
1371
|
+
const capture = async (req, res) => {
|
|
1372
|
+
const origin = req.headers.origin;
|
|
1373
|
+
if (origin !== void 0 && !ownOrigin(origin, req, protocol, server)) {
|
|
1374
|
+
return json(res, 403, { error: "a capture is only taken for this server\u2019s own pages" });
|
|
1375
|
+
}
|
|
1376
|
+
const type = (req.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1377
|
+
if (type !== "application/json") return json(res, 415, { error: "a capture payload is sent as application/json" });
|
|
1378
|
+
const body2 = await readBody(req);
|
|
1379
|
+
if (body2 === null) {
|
|
1380
|
+
res.setHeader("Connection", "close");
|
|
1381
|
+
json(res, 413, { error: "capture payload is too large" });
|
|
1382
|
+
return void res.once("finish", () => req.destroy());
|
|
1383
|
+
}
|
|
1384
|
+
let parsed;
|
|
1385
|
+
try {
|
|
1386
|
+
parsed = JSON.parse(body2);
|
|
1387
|
+
} catch {
|
|
1388
|
+
return json(res, 400, { error: "capture payload is not JSON" });
|
|
1389
|
+
}
|
|
1390
|
+
const payload = parsePayload(parsed);
|
|
1391
|
+
if ("error" in payload) return json(res, 400, payload);
|
|
1392
|
+
const key = keyOf2(payload);
|
|
1393
|
+
let png = cache.get(key);
|
|
1394
|
+
if (!png) {
|
|
1395
|
+
const result = await browser();
|
|
1396
|
+
if ("unavailable" in result) return json(res, 503, { available: false, reason: result.unavailable });
|
|
1397
|
+
const page = `${socketOrigin(req, protocol)}${base}${payload.isolate ? CAPTURE_PAGES.frame : CAPTURE_PAGES.shell}`;
|
|
1398
|
+
const at = revision;
|
|
1399
|
+
try {
|
|
1400
|
+
png = await result.capture(page, payload, allowOrigins());
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
const message = messageOf(err);
|
|
1403
|
+
if (message === QUEUE_FULL) return json(res, 503, { available: false, reason: QUEUE_FULL });
|
|
1404
|
+
return json(res, 500, { error: message });
|
|
1405
|
+
}
|
|
1406
|
+
if (at === revision) cache.set(key, png);
|
|
1407
|
+
}
|
|
1408
|
+
res.statusCode = 200;
|
|
1409
|
+
res.setHeader("Content-Type", "image/png");
|
|
1410
|
+
res.end(png);
|
|
1411
|
+
};
|
|
1412
|
+
const status = async (res) => {
|
|
1413
|
+
const result = await captureAvailability();
|
|
1414
|
+
json(res, 200, "unavailable" in result ? { available: false, reason: result.unavailable } : { available: true });
|
|
1415
|
+
};
|
|
1416
|
+
server.middlewares.use((req, res, next) => {
|
|
1417
|
+
if (req.url?.split("?")[0] !== `${base}${CAPTURE_PATH}`) return next();
|
|
1418
|
+
if (req.method === "GET") return void status(res).catch(next);
|
|
1419
|
+
if (req.method === "POST") return void capture(req, res).catch(next);
|
|
1420
|
+
next();
|
|
1421
|
+
});
|
|
1422
|
+
return {
|
|
1423
|
+
revise() {
|
|
1424
|
+
revision += 1;
|
|
1425
|
+
cache.clear();
|
|
1426
|
+
},
|
|
1427
|
+
async close() {
|
|
1428
|
+
revision += 1;
|
|
1429
|
+
cache.clear();
|
|
1430
|
+
const closing = launched;
|
|
1431
|
+
launched = null;
|
|
1432
|
+
const result = await closing;
|
|
1433
|
+
if (result && !("unavailable" in result)) await result.close();
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1151
1437
|
|
|
1152
1438
|
// src/vite/servePages.ts
|
|
1153
|
-
var PAGES2 = {
|
|
1154
|
-
|
|
1439
|
+
var PAGES2 = {
|
|
1440
|
+
"/": "shell-entry.js",
|
|
1441
|
+
"/index.html": "shell-entry.js",
|
|
1442
|
+
"/frame.html": "frame-entry.js",
|
|
1443
|
+
[`/${CAPTURE_PAGES.shell}`]: "capture-entry.js",
|
|
1444
|
+
[`/${CAPTURE_PAGES.frame}`]: "capture-frame-entry.js"
|
|
1445
|
+
};
|
|
1446
|
+
function servePages(server, base, shell) {
|
|
1155
1447
|
server.middlewares.use((req, res, next) => {
|
|
1156
1448
|
const url = req.url ?? "/";
|
|
1157
1449
|
const path = url.split("?")[0] ?? "/";
|
|
@@ -1159,7 +1451,7 @@ function servePages(server, base, title) {
|
|
|
1159
1451
|
const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
|
|
1160
1452
|
const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
|
|
1161
1453
|
if (!entry) return next();
|
|
1162
|
-
server.transformIndexHtml(url, html(entry, "serve",
|
|
1454
|
+
server.transformIndexHtml(url, html(entry, "serve", shell().title)).then(
|
|
1163
1455
|
(doc) => {
|
|
1164
1456
|
res.statusCode = 200;
|
|
1165
1457
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -1168,15 +1460,14 @@ function servePages(server, base, title) {
|
|
|
1168
1460
|
(err) => next(err)
|
|
1169
1461
|
);
|
|
1170
1462
|
});
|
|
1463
|
+
return serveCapture(server, base, () => shell().allowOrigins);
|
|
1171
1464
|
}
|
|
1172
|
-
|
|
1173
|
-
// src/vite/shellTitle.ts
|
|
1174
|
-
function shellTitle(code, file) {
|
|
1465
|
+
function configOf(code, file) {
|
|
1175
1466
|
let program;
|
|
1176
1467
|
try {
|
|
1177
1468
|
program = parseFile(code, file);
|
|
1178
1469
|
} catch {
|
|
1179
|
-
return
|
|
1470
|
+
return null;
|
|
1180
1471
|
}
|
|
1181
1472
|
const bindings = /* @__PURE__ */ new Map();
|
|
1182
1473
|
let config;
|
|
@@ -1191,7 +1482,46 @@ function shellTitle(code, file) {
|
|
|
1191
1482
|
}
|
|
1192
1483
|
}
|
|
1193
1484
|
const wrapper = calleeOf(config, bindings);
|
|
1194
|
-
return
|
|
1485
|
+
return { config: objectOf(config, bindings, new Set(wrapper ? [wrapper] : [])), bindings };
|
|
1486
|
+
}
|
|
1487
|
+
function shellTitle(code, file) {
|
|
1488
|
+
const read = configOf(code, file);
|
|
1489
|
+
return read ? stringOf(prop(read.config, "title"), read.bindings) : void 0;
|
|
1490
|
+
}
|
|
1491
|
+
function arrayOf(node, bindings, depth = 0) {
|
|
1492
|
+
if (!node || depth > 16) return null;
|
|
1493
|
+
if (node.type === "ArrayExpression") return node;
|
|
1494
|
+
if (node.type === "Identifier") return arrayOf(bindings.get(node.name), bindings, depth + 1);
|
|
1495
|
+
if ("expression" in node && (node.type.startsWith("TS") || node.type === "ParenthesizedExpression")) {
|
|
1496
|
+
return arrayOf(node.expression, bindings, depth + 1);
|
|
1497
|
+
}
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
var originOf = (url) => {
|
|
1501
|
+
if (url === void 0) return [];
|
|
1502
|
+
try {
|
|
1503
|
+
const { origin } = new URL(url);
|
|
1504
|
+
return origin === "null" ? [] : [origin];
|
|
1505
|
+
} catch {
|
|
1506
|
+
return [];
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
function shellAllowOrigins(code, file) {
|
|
1510
|
+
const read = configOf(code, file);
|
|
1511
|
+
if (!read) return [];
|
|
1512
|
+
const capture = objectOf(prop(read.config, "capture"), read.bindings, /* @__PURE__ */ new Set());
|
|
1513
|
+
const list = arrayOf(prop(capture, "allowOrigins"), read.bindings);
|
|
1514
|
+
return (list?.elements ?? []).flatMap((el) => el && el.type !== "SpreadElement" ? originOf(stringOf(el, read.bindings)) : []);
|
|
1515
|
+
}
|
|
1516
|
+
function readShellFields(file) {
|
|
1517
|
+
let code;
|
|
1518
|
+
try {
|
|
1519
|
+
if (file === void 0) throw new Error("no shell config");
|
|
1520
|
+
code = readFileSync(file, "utf8");
|
|
1521
|
+
} catch {
|
|
1522
|
+
return { title: void 0, allowOrigins: [] };
|
|
1523
|
+
}
|
|
1524
|
+
return { title: shellTitle(code, file), allowOrigins: shellAllowOrigins(code, file) };
|
|
1195
1525
|
}
|
|
1196
1526
|
var SHIMS = [
|
|
1197
1527
|
[/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
|
|
@@ -1208,6 +1538,105 @@ function storybookShims() {
|
|
|
1208
1538
|
}
|
|
1209
1539
|
};
|
|
1210
1540
|
}
|
|
1541
|
+
|
|
1542
|
+
// src/vite/capture/entries.ts
|
|
1543
|
+
var body = (mount) => `import index from 'virtual:forge/index.js';
|
|
1544
|
+
import importers from 'virtual:forge/importers.js';
|
|
1545
|
+
import setup from 'virtual:forge/frame-config.js';
|
|
1546
|
+
|
|
1547
|
+
${mount}
|
|
1548
|
+
// The endpoint drops its captures on any edit; a page loaded before one would render the old module.
|
|
1549
|
+
import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
1550
|
+
`;
|
|
1551
|
+
function captureEntry() {
|
|
1552
|
+
return `import { mountCapture } from '@weasel-js/forge/frame';
|
|
1553
|
+
import '@weasel-js/labkit/styles.css';
|
|
1554
|
+
import '@weasel-js/forge/shell.css';
|
|
1555
|
+
import config from 'virtual:forge/shell-config.js';
|
|
1556
|
+
${body("mountCapture({ index, importers, setup, labTheme: config.labTheme });")}`;
|
|
1557
|
+
}
|
|
1558
|
+
function captureFrameEntry(styles) {
|
|
1559
|
+
return `import { mountCapture } from '@weasel-js/forge/frame';
|
|
1560
|
+
import '@weasel-js/forge/frame.css';
|
|
1561
|
+
${styles.map((path) => `import ${JSON.stringify(path)};
|
|
1562
|
+
`).join("")}${body("mountCapture({ index, importers, setup });")}`;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// src/vite/entries.ts
|
|
1566
|
+
function shellEntry(base, stories) {
|
|
1567
|
+
return `import { followEdits, mountWorkshop } from '@weasel-js/forge/shell';
|
|
1568
|
+
import '@weasel-js/labkit/styles.css';
|
|
1569
|
+
import '@weasel-js/forge/shell.css';
|
|
1570
|
+
import index from 'virtual:forge/index.js';
|
|
1571
|
+
import importers from 'virtual:forge/importers.js';
|
|
1572
|
+
import config from 'virtual:forge/shell-config.js';
|
|
1573
|
+
import setup from 'virtual:forge/frame-config.js';
|
|
1574
|
+
|
|
1575
|
+
const workshop = mountWorkshop({
|
|
1576
|
+
index,
|
|
1577
|
+
importers,
|
|
1578
|
+
setup,
|
|
1579
|
+
config,
|
|
1580
|
+
frameUrl: ${JSON.stringify(`${base}frame.html`)},
|
|
1581
|
+
captureBase: ${JSON.stringify(base)},
|
|
1582
|
+
stories: ${JSON.stringify(stories)},
|
|
1583
|
+
});
|
|
1584
|
+
// Read from source on first request, so the graph costs nothing until the page is up.
|
|
1585
|
+
import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
|
|
1586
|
+
const edits = followEdits(import.meta.hot, workshop);
|
|
1587
|
+
import.meta.hot?.accept('virtual:forge/importers.js', (next) => edits.importers(next?.default));
|
|
1588
|
+
`;
|
|
1589
|
+
}
|
|
1590
|
+
function frameEntry(styles) {
|
|
1591
|
+
return `import { mountFrame } from '@weasel-js/forge/frame';
|
|
1592
|
+
import '@weasel-js/forge/frame.css';
|
|
1593
|
+
${styles.map((path) => `import ${JSON.stringify(path)};
|
|
1594
|
+
`).join("")}import index from 'virtual:forge/index.js';
|
|
1595
|
+
import importers from 'virtual:forge/importers.js';
|
|
1596
|
+
import setup from 'virtual:forge/frame-config.js';
|
|
1597
|
+
|
|
1598
|
+
mountFrame({ index, importers, setup });
|
|
1599
|
+
// The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
|
|
1600
|
+
// showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
|
|
1601
|
+
import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
|
|
1602
|
+
`;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/vite/virtualModules.ts
|
|
1606
|
+
var PREFIX = "virtual:forge/";
|
|
1607
|
+
var MODULES = /* @__PURE__ */ new Set([
|
|
1608
|
+
"index.js",
|
|
1609
|
+
"importers.js",
|
|
1610
|
+
"deps.js",
|
|
1611
|
+
"frame-config.js",
|
|
1612
|
+
"shell-config.js",
|
|
1613
|
+
"shell-entry.js",
|
|
1614
|
+
"frame-entry.js",
|
|
1615
|
+
"capture-entry.js",
|
|
1616
|
+
"capture-frame-entry.js"
|
|
1617
|
+
]);
|
|
1618
|
+
function virtualModules(options, src) {
|
|
1619
|
+
const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(src.root(), path))};
|
|
1620
|
+
` : "export default {};\n";
|
|
1621
|
+
const frameStyles = () => (options.frameStyles ?? []).map((path) => resolve(src.root(), path));
|
|
1622
|
+
return {
|
|
1623
|
+
"index.js": () => `export default ${JSON.stringify(src.index())};
|
|
1624
|
+
`,
|
|
1625
|
+
"importers.js": () => {
|
|
1626
|
+
const lines = [...src.files()].filter(([, entries]) => entries.length > 0).map(([file]) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(file)}),`);
|
|
1627
|
+
return `export default {
|
|
1628
|
+
${lines.join("\n")}
|
|
1629
|
+
};
|
|
1630
|
+
`;
|
|
1631
|
+
},
|
|
1632
|
+
"frame-config.js": () => configModule(options.frameConfig),
|
|
1633
|
+
"shell-config.js": () => configModule(options.shellConfig),
|
|
1634
|
+
"shell-entry.js": () => shellEntry(src.base(), options.stories),
|
|
1635
|
+
"frame-entry.js": () => frameEntry(frameStyles()),
|
|
1636
|
+
"capture-entry.js": () => captureEntry(),
|
|
1637
|
+
"capture-frame-entry.js": () => captureFrameEntry(frameStyles())
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1211
1640
|
var WAIT_MS = 5e3;
|
|
1212
1641
|
var REWRITE_MS = 250;
|
|
1213
1642
|
var watchDir = (dir, onEvent) => {
|
|
@@ -1219,7 +1648,7 @@ var watchDir = (dir, onEvent) => {
|
|
|
1219
1648
|
};
|
|
1220
1649
|
function fileEventsArrive({ waitMs = WAIT_MS, watch = watchDir } = {}) {
|
|
1221
1650
|
const dir = mkdtempSync(join(tmpdir(), "forge-watch-"));
|
|
1222
|
-
return new Promise((
|
|
1651
|
+
return new Promise((resolve6) => {
|
|
1223
1652
|
let settled2 = false;
|
|
1224
1653
|
let writes = 0;
|
|
1225
1654
|
let stop;
|
|
@@ -1232,7 +1661,7 @@ function fileEventsArrive({ waitMs = WAIT_MS, watch = watchDir } = {}) {
|
|
|
1232
1661
|
clearTimeout(deadline);
|
|
1233
1662
|
stop?.();
|
|
1234
1663
|
rmSync(dir, { recursive: true, force: true });
|
|
1235
|
-
|
|
1664
|
+
resolve6(arrived);
|
|
1236
1665
|
};
|
|
1237
1666
|
const write = () => writeFileSync(join(dir, "probe"), String(writes++));
|
|
1238
1667
|
try {
|
|
@@ -1265,8 +1694,6 @@ function watchProbe({ probe } = {}) {
|
|
|
1265
1694
|
}
|
|
1266
1695
|
|
|
1267
1696
|
// src/vite/plugin.ts
|
|
1268
|
-
var PREFIX = "virtual:forge/";
|
|
1269
|
-
var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
|
|
1270
1697
|
var SCRIPT2 = /\.[cm]?[jt]sx?$/;
|
|
1271
1698
|
function forge(options) {
|
|
1272
1699
|
let root = process.cwd();
|
|
@@ -1337,32 +1764,9 @@ function forge(options) {
|
|
|
1337
1764
|
}
|
|
1338
1765
|
return graph;
|
|
1339
1766
|
}
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
try {
|
|
1344
|
-
return shellTitle(readFileSync(file, "utf8"), file);
|
|
1345
|
-
} catch {
|
|
1346
|
-
return void 0;
|
|
1347
|
-
}
|
|
1348
|
-
};
|
|
1349
|
-
const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
|
|
1350
|
-
` : "export default {};\n";
|
|
1351
|
-
const modules = {
|
|
1352
|
-
"index.js": () => `export default ${JSON.stringify(index())};
|
|
1353
|
-
`,
|
|
1354
|
-
"importers.js": () => {
|
|
1355
|
-
const lines = [...files()].filter(([, entries]) => entries.length > 0).map(([file]) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(file)}),`);
|
|
1356
|
-
return `export default {
|
|
1357
|
-
${lines.join("\n")}
|
|
1358
|
-
};
|
|
1359
|
-
`;
|
|
1360
|
-
},
|
|
1361
|
-
"frame-config.js": () => configModule(options.frameConfig),
|
|
1362
|
-
"shell-config.js": () => configModule(options.shellConfig),
|
|
1363
|
-
"shell-entry.js": () => shellEntry(base, options.stories),
|
|
1364
|
-
"frame-entry.js": () => frameEntry((options.frameStyles ?? []).map((path) => resolve(root, path)))
|
|
1365
|
-
};
|
|
1767
|
+
const shellUnder = (at) => readShellFields(options.shellConfig ? resolve(at, options.shellConfig) : void 0);
|
|
1768
|
+
const modules = virtualModules(options, { index, files, root: () => root, base: () => base });
|
|
1769
|
+
let capture = null;
|
|
1366
1770
|
const invalidate = (server, names) => {
|
|
1367
1771
|
for (const env of Object.values(server.environments)) {
|
|
1368
1772
|
for (const name of names) {
|
|
@@ -1373,12 +1777,12 @@ ${lines.join("\n")}
|
|
|
1373
1777
|
};
|
|
1374
1778
|
function redeps(server) {
|
|
1375
1779
|
if (depsSent === null || depsPending) return;
|
|
1376
|
-
const
|
|
1780
|
+
const resolve6 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
|
|
1377
1781
|
depsPending = Promise.resolve().then(async () => {
|
|
1378
|
-
const graph = await graphOf(
|
|
1379
|
-
const
|
|
1380
|
-
if (
|
|
1381
|
-
depsSent =
|
|
1782
|
+
const graph = await graphOf(resolve6);
|
|
1783
|
+
const json2 = JSON.stringify(graph);
|
|
1784
|
+
if (json2 === depsSent) return;
|
|
1785
|
+
depsSent = json2;
|
|
1382
1786
|
invalidate(server, ["deps.js"]);
|
|
1383
1787
|
server.ws.send({ type: "custom", event: "forge:deps", data: graph });
|
|
1384
1788
|
}).catch(logError).finally(() => {
|
|
@@ -1442,7 +1846,7 @@ ${lines.join("\n")}
|
|
|
1442
1846
|
];
|
|
1443
1847
|
const optimizeDeps = { entries };
|
|
1444
1848
|
if (command !== "build") return { optimizeDeps };
|
|
1445
|
-
return { optimizeDeps, build: { rolldownOptions: { input: writePages(at,
|
|
1849
|
+
return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, shellUnder(at).title) } } };
|
|
1446
1850
|
},
|
|
1447
1851
|
generateBundle: {
|
|
1448
1852
|
order: "post",
|
|
@@ -1479,6 +1883,7 @@ ${lines.join("\n")}
|
|
|
1479
1883
|
},
|
|
1480
1884
|
hotUpdate({ modules: modules2, server }) {
|
|
1481
1885
|
if (this.environment.name !== "client") return;
|
|
1886
|
+
capture?.revise();
|
|
1482
1887
|
for (const file of storiesReaching(modules2)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
|
|
1483
1888
|
},
|
|
1484
1889
|
configureServer(server) {
|
|
@@ -1495,8 +1900,9 @@ ${lines.join("\n")}
|
|
|
1495
1900
|
}, logError);
|
|
1496
1901
|
});
|
|
1497
1902
|
}
|
|
1498
|
-
servePages(server, base, () =>
|
|
1499
|
-
}
|
|
1903
|
+
capture = servePages(server, base, () => shellUnder(root));
|
|
1904
|
+
},
|
|
1905
|
+
closeServer: () => capture?.close()
|
|
1500
1906
|
}
|
|
1501
1907
|
];
|
|
1502
1908
|
}
|
|
@@ -1540,7 +1946,6 @@ function forgeTest(options) {
|
|
|
1540
1946
|
const lines = [
|
|
1541
1947
|
"",
|
|
1542
1948
|
`import { test as __forge_test } from "vitest";`,
|
|
1543
|
-
`import { page as __forge_page } from "vitest/browser";`,
|
|
1544
1949
|
`import { runStory as __forge_runStory } from "@weasel-js/forge/test";`,
|
|
1545
1950
|
// shell.css draws the story host; frame.css still holds the boundary's fault rule.
|
|
1546
1951
|
`import "@weasel-js/forge/shell.css";`,
|
|
@@ -1551,7 +1956,6 @@ function forgeTest(options) {
|
|
|
1551
1956
|
`const __forge_timeout = ${TIMEOUT_MS};`,
|
|
1552
1957
|
"const __forge_options = {",
|
|
1553
1958
|
` setup: ${options.frameConfig ? "__forge_frame_config" : "undefined"},`,
|
|
1554
|
-
" viewport: (width, height) => __forge_page.viewport(width, height),",
|
|
1555
1959
|
"};",
|
|
1556
1960
|
"const __forge_run = async (exportName) => __forge_runStory(await import(/* @vite-ignore */ import.meta.url), exportName, __forge_file, __forge_title, __forge_options);",
|
|
1557
1961
|
// An isolated story asks for a document of its own, which the test page is not; its reason goes in the name.
|