@weasel-js/forge 1.11.1 → 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.
Files changed (44) hide show
  1. package/README.md +90 -0
  2. package/dist/{FrameController-Cj_i-Rdk.d.ts → FrameController-CdnaIo4C.d.ts} +1 -1
  3. package/dist/{chunk-7BQPHMA2.js → chunk-3BAM6M26.js} +2 -2
  4. package/dist/chunk-3BAM6M26.js.map +1 -0
  5. package/dist/{chunk-ZAWXMUGZ.js → chunk-5WS67HMU.js} +8 -3
  6. package/dist/chunk-5WS67HMU.js.map +1 -0
  7. package/dist/{chunk-BKPDKUDN.js → chunk-ATI7FYJ7.js} +8 -5
  8. package/dist/chunk-ATI7FYJ7.js.map +1 -0
  9. package/dist/chunk-ORMMZ5PV.js +59 -0
  10. package/dist/chunk-ORMMZ5PV.js.map +1 -0
  11. package/dist/{chunk-JTCUIJPZ.js → chunk-WMGEO5M6.js} +49 -7
  12. package/dist/chunk-WMGEO5M6.js.map +1 -0
  13. package/dist/chunk-YDLRSEUN.js +501 -0
  14. package/dist/chunk-YDLRSEUN.js.map +1 -0
  15. package/dist/{config-DN6IaNf6.d.ts → config-Mg05Ja9r.d.ts} +18 -1
  16. package/dist/frame/index.css +11 -0
  17. package/dist/frame/index.css.map +1 -1
  18. package/dist/frame/index.d.ts +62 -29
  19. package/dist/frame/index.js +134 -6
  20. package/dist/frame/index.js.map +1 -1
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
  25. package/dist/shell/index.css +116 -0
  26. package/dist/shell/index.css.map +1 -1
  27. package/dist/shell/index.d.ts +43 -7
  28. package/dist/shell/index.js +1570 -259
  29. package/dist/shell/index.js.map +1 -1
  30. package/dist/test/index.css +58 -0
  31. package/dist/test/index.css.map +1 -0
  32. package/dist/test/index.d.ts +1 -1
  33. package/dist/test/index.js +4 -4
  34. package/dist/vite/index.js +501 -106
  35. package/dist/vite/index.js.map +1 -1
  36. package/package.json +13 -8
  37. package/dist/chunk-5DXUNLV2.js +0 -6
  38. package/dist/chunk-5DXUNLV2.js.map +0 -1
  39. package/dist/chunk-7BQPHMA2.js.map +0 -1
  40. package/dist/chunk-BKPDKUDN.js.map +0 -1
  41. package/dist/chunk-FZNABWJC.js +0 -282
  42. package/dist/chunk-FZNABWJC.js.map +0 -1
  43. package/dist/chunk-JTCUIJPZ.js.map +0 -1
  44. package/dist/chunk-ZAWXMUGZ.js.map +0 -1
@@ -1,11 +1,13 @@
1
- import { DEFAULT_TITLE } from '../chunk-5DXUNLV2.js';
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-ZAWXMUGZ.js';
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, resolve5) {
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 = resolve5(spec, importer).then(
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;
@@ -853,13 +855,13 @@ function createTracer(read) {
853
855
  }
854
856
  return modules.get(file) ?? null;
855
857
  };
856
- return (resolve5) => {
858
+ return (resolve6) => {
857
859
  const trace = {
858
860
  reads: /* @__PURE__ */ new Set(),
859
861
  unresolved: false,
860
862
  info,
861
863
  async target(spec, importer) {
862
- const id = (await resolve5(spec, importer).catch(() => null))?.split("?")[0];
864
+ const id = (await resolve6(spec, importer).catch(() => null))?.split("?")[0];
863
865
  if (!id) trace.unresolved = true;
864
866
  return id && !id.startsWith("\0") ? id : null;
865
867
  },
@@ -901,8 +903,8 @@ function sameResolution(a, b) {
901
903
  }
902
904
  function createWrapperResolver(read) {
903
905
  const tracer = createTracer(read);
904
- return async (file, callees, resolve5) => {
905
- const trace = tracer(resolve5);
906
+ return async (file, callees, resolve6) => {
907
+ const trace = tracer(resolve6);
906
908
  const wrappers = /* @__PURE__ */ new Set();
907
909
  const direct = [...trace.info(file)?.imports.values() ?? []].some((imp) => imp.spec === "@weasel-js/forge");
908
910
  if (callees.length === 0 && !direct) return { wrappers, foreign: NO_FOREIGN, reads: trace.reads, unresolved: false };
@@ -929,8 +931,8 @@ function indexStory(code, file, source, resolved) {
929
931
  const foreign = resolved ? foreignFrom(resolved.foreign) : void 0;
930
932
  return indexFile(code, file, autoTitle(file, source.root, source.stories), resolved?.wrappers, foreign);
931
933
  }
932
- async function indexStoryResolved(code, file, source, resolve5, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
933
- const resolution = await decide(file, foreignCallees(code, file), resolve5);
934
+ async function indexStoryResolved(code, file, source, resolve6, decide = createWrapperResolver((path) => readFileSync(path, "utf8"))) {
935
+ const resolution = await decide(file, foreignCallees(code, file), resolve6);
934
936
  return { entries: indexStory(code, file, source, resolution), resolution };
935
937
  }
936
938
  function ownEntries(realms) {
@@ -945,56 +947,6 @@ function ownEntries(realms) {
945
947
  }
946
948
  return [];
947
949
  }
948
-
949
- // src/vite/entries.ts
950
- function shellEntry(base, stories) {
951
- return `import { mountWorkshop } from '@weasel-js/forge/shell';
952
- import '@weasel-js/labkit/styles.css';
953
- import '@weasel-js/forge/shell.css';
954
- import index from 'virtual:forge/index.js';
955
- import importers from 'virtual:forge/importers.js';
956
- import config from 'virtual:forge/shell-config.js';
957
- import setup from 'virtual:forge/frame-config.js';
958
-
959
- const workshop = mountWorkshop({
960
- index,
961
- importers,
962
- setup,
963
- config,
964
- frameUrl: ${JSON.stringify(`${base}frame.html`)},
965
- stories: ${JSON.stringify(stories)},
966
- });
967
- import.meta.hot?.on('forge:index', (next) => workshop.setIndex(next));
968
- // Read from source on first request, so the graph costs nothing until the page is up.
969
- import('virtual:forge/deps.js').then((m) => workshop.setDependencies(m.default));
970
- import.meta.hot?.on('forge:deps', (next) => workshop.setDependencies(next));
971
- // An edit to a story file, or to a module one imports, reaches here as an update of the importers module, whose
972
- // fresh import() URLs carry the new timestamps; the files it reaches arrive just before, so the reload waits for
973
- // the importers that can fetch them.
974
- const stale = new Set();
975
- import.meta.hot?.on('forge:story', ({ file }) => stale.add(file));
976
- import.meta.hot?.accept('virtual:forge/importers.js', (next) => {
977
- if (!next) return;
978
- workshop.setImporters(next.default);
979
- for (const file of stale) workshop.reloadStory(file);
980
- stale.clear();
981
- });
982
- `;
983
- }
984
- function frameEntry(styles) {
985
- return `import { mountFrame } from '@weasel-js/forge/frame';
986
- import '@weasel-js/forge/frame.css';
987
- ${styles.map((path) => `import ${JSON.stringify(path)};
988
- `).join("")}import index from 'virtual:forge/index.js';
989
- import importers from 'virtual:forge/importers.js';
990
- import setup from 'virtual:forge/frame-config.js';
991
-
992
- mountFrame({ index, importers, setup });
993
- // The importers module is shared with the workshop page, which accepts a story edit in place; a frame document
994
- // showing the old module reloads instead, and without this the edit would dead-end here and reload every page.
995
- import.meta.hot?.accept('virtual:forge/importers.js', () => location.reload());
996
- `;
997
- }
998
950
  var PLATFORM = /[\\/]node_modules[\\/](@types[\\/]react(-dom)?|typescript)[\\/]/;
999
951
  async function createPropReader(root, stories, { skip } = {}) {
1000
952
  const skipped = (file) => PLATFORM.test(file) || skip?.(file) === true;
@@ -1159,10 +1111,339 @@ export const ${PROP_ARG_TYPES_EXPORT} = ${JSON.stringify(table.argTypes)};
1159
1111
  }
1160
1112
  };
1161
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
+ }
1162
1437
 
1163
1438
  // src/vite/servePages.ts
1164
- var PAGES2 = { "/": "shell-entry.js", "/index.html": "shell-entry.js", "/frame.html": "frame-entry.js" };
1165
- function servePages(server, base, title) {
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) {
1166
1447
  server.middlewares.use((req, res, next) => {
1167
1448
  const url = req.url ?? "/";
1168
1449
  const path = url.split("?")[0] ?? "/";
@@ -1170,7 +1451,7 @@ function servePages(server, base, title) {
1170
1451
  const page = path === prefix ? "/" : path.startsWith(`${prefix}/`) ? path.slice(prefix.length) : null;
1171
1452
  const entry = req.method === "GET" && page !== null ? PAGES2[page] : void 0;
1172
1453
  if (!entry) return next();
1173
- server.transformIndexHtml(url, html(entry, "serve", title())).then(
1454
+ server.transformIndexHtml(url, html(entry, "serve", shell().title)).then(
1174
1455
  (doc) => {
1175
1456
  res.statusCode = 200;
1176
1457
  res.setHeader("Content-Type", "text/html");
@@ -1179,15 +1460,14 @@ function servePages(server, base, title) {
1179
1460
  (err) => next(err)
1180
1461
  );
1181
1462
  });
1463
+ return serveCapture(server, base, () => shell().allowOrigins);
1182
1464
  }
1183
-
1184
- // src/vite/shellTitle.ts
1185
- function shellTitle(code, file) {
1465
+ function configOf(code, file) {
1186
1466
  let program;
1187
1467
  try {
1188
1468
  program = parseFile(code, file);
1189
1469
  } catch {
1190
- return void 0;
1470
+ return null;
1191
1471
  }
1192
1472
  const bindings = /* @__PURE__ */ new Map();
1193
1473
  let config;
@@ -1202,7 +1482,46 @@ function shellTitle(code, file) {
1202
1482
  }
1203
1483
  }
1204
1484
  const wrapper = calleeOf(config, bindings);
1205
- return stringOf(prop(objectOf(config, bindings, new Set(wrapper ? [wrapper] : [])), "title"), bindings);
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) };
1206
1525
  }
1207
1526
  var SHIMS = [
1208
1527
  [/^@?storybook\/preview-api$/, "@weasel-js/forge/preview-api"],
@@ -1219,6 +1538,105 @@ function storybookShims() {
1219
1538
  }
1220
1539
  };
1221
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
+ }
1222
1640
  var WAIT_MS = 5e3;
1223
1641
  var REWRITE_MS = 250;
1224
1642
  var watchDir = (dir, onEvent) => {
@@ -1230,7 +1648,7 @@ var watchDir = (dir, onEvent) => {
1230
1648
  };
1231
1649
  function fileEventsArrive({ waitMs = WAIT_MS, watch = watchDir } = {}) {
1232
1650
  const dir = mkdtempSync(join(tmpdir(), "forge-watch-"));
1233
- return new Promise((resolve5) => {
1651
+ return new Promise((resolve6) => {
1234
1652
  let settled2 = false;
1235
1653
  let writes = 0;
1236
1654
  let stop;
@@ -1243,7 +1661,7 @@ function fileEventsArrive({ waitMs = WAIT_MS, watch = watchDir } = {}) {
1243
1661
  clearTimeout(deadline);
1244
1662
  stop?.();
1245
1663
  rmSync(dir, { recursive: true, force: true });
1246
- resolve5(arrived);
1664
+ resolve6(arrived);
1247
1665
  };
1248
1666
  const write = () => writeFileSync(join(dir, "probe"), String(writes++));
1249
1667
  try {
@@ -1276,8 +1694,6 @@ function watchProbe({ probe } = {}) {
1276
1694
  }
1277
1695
 
1278
1696
  // src/vite/plugin.ts
1279
- var PREFIX = "virtual:forge/";
1280
- var MODULES = /* @__PURE__ */ new Set(["index.js", "importers.js", "deps.js", "frame-config.js", "shell-config.js", "shell-entry.js", "frame-entry.js"]);
1281
1697
  var SCRIPT2 = /\.[cm]?[jt]sx?$/;
1282
1698
  function forge(options) {
1283
1699
  let root = process.cwd();
@@ -1348,32 +1764,9 @@ function forge(options) {
1348
1764
  }
1349
1765
  return graph;
1350
1766
  }
1351
- const titleUnder = (at) => {
1352
- if (!options.shellConfig) return void 0;
1353
- const file = resolve(at, options.shellConfig);
1354
- try {
1355
- return shellTitle(readFileSync(file, "utf8"), file);
1356
- } catch {
1357
- return void 0;
1358
- }
1359
- };
1360
- const configModule = (path) => path ? `export { default } from ${JSON.stringify(resolve(root, path))};
1361
- ` : "export default {};\n";
1362
- const modules = {
1363
- "index.js": () => `export default ${JSON.stringify(index())};
1364
- `,
1365
- "importers.js": () => {
1366
- const lines = [...files()].filter(([, entries]) => entries.length > 0).map(([file]) => ` ${JSON.stringify(file)}: () => import(${JSON.stringify(file)}),`);
1367
- return `export default {
1368
- ${lines.join("\n")}
1369
- };
1370
- `;
1371
- },
1372
- "frame-config.js": () => configModule(options.frameConfig),
1373
- "shell-config.js": () => configModule(options.shellConfig),
1374
- "shell-entry.js": () => shellEntry(base, options.stories),
1375
- "frame-entry.js": () => frameEntry((options.frameStyles ?? []).map((path) => resolve(root, path)))
1376
- };
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;
1377
1770
  const invalidate = (server, names) => {
1378
1771
  for (const env of Object.values(server.environments)) {
1379
1772
  for (const name of names) {
@@ -1384,12 +1777,12 @@ ${lines.join("\n")}
1384
1777
  };
1385
1778
  function redeps(server) {
1386
1779
  if (depsSent === null || depsPending) return;
1387
- const resolve5 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
1780
+ const resolve6 = async (spec, importer) => (await server.environments.client.pluginContainer.resolveId(spec, importer))?.id ?? null;
1388
1781
  depsPending = Promise.resolve().then(async () => {
1389
- const graph = await graphOf(resolve5);
1390
- const json = JSON.stringify(graph);
1391
- if (json === depsSent) return;
1392
- depsSent = json;
1782
+ const graph = await graphOf(resolve6);
1783
+ const json2 = JSON.stringify(graph);
1784
+ if (json2 === depsSent) return;
1785
+ depsSent = json2;
1393
1786
  invalidate(server, ["deps.js"]);
1394
1787
  server.ws.send({ type: "custom", event: "forge:deps", data: graph });
1395
1788
  }).catch(logError).finally(() => {
@@ -1453,7 +1846,7 @@ ${lines.join("\n")}
1453
1846
  ];
1454
1847
  const optimizeDeps = { entries };
1455
1848
  if (command !== "build") return { optimizeDeps };
1456
- return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, titleUnder(at)) } } };
1849
+ return { optimizeDeps, build: { rolldownOptions: { input: writePages(at, shellUnder(at).title) } } };
1457
1850
  },
1458
1851
  generateBundle: {
1459
1852
  order: "post",
@@ -1490,6 +1883,7 @@ ${lines.join("\n")}
1490
1883
  },
1491
1884
  hotUpdate({ modules: modules2, server }) {
1492
1885
  if (this.environment.name !== "client") return;
1886
+ capture?.revise();
1493
1887
  for (const file of storiesReaching(modules2)) server.ws.send({ type: "custom", event: "forge:story", data: { file } });
1494
1888
  },
1495
1889
  configureServer(server) {
@@ -1506,8 +1900,9 @@ ${lines.join("\n")}
1506
1900
  }, logError);
1507
1901
  });
1508
1902
  }
1509
- servePages(server, base, () => titleUnder(root));
1510
- }
1903
+ capture = servePages(server, base, () => shellUnder(root));
1904
+ },
1905
+ closeServer: () => capture?.close()
1511
1906
  }
1512
1907
  ];
1513
1908
  }