@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.
- package/README.md +90 -0
- package/dist/{FrameController-Cj_i-Rdk.d.ts → FrameController-CdnaIo4C.d.ts} +1 -1
- 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-BKPDKUDN.js → chunk-ATI7FYJ7.js} +8 -5
- package/dist/chunk-ATI7FYJ7.js.map +1 -0
- package/dist/chunk-ORMMZ5PV.js +59 -0
- package/dist/chunk-ORMMZ5PV.js.map +1 -0
- package/dist/{chunk-JTCUIJPZ.js → chunk-WMGEO5M6.js} +49 -7
- 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-DN6IaNf6.d.ts → config-Mg05Ja9r.d.ts} +18 -1
- package/dist/frame/index.css +11 -0
- package/dist/frame/index.css.map +1 -1
- package/dist/frame/index.d.ts +62 -29
- package/dist/frame/index.js +134 -6
- package/dist/frame/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mountFrame-Ds3QLnBF.d.ts +35 -0
- package/dist/shell/index.css +116 -0
- package/dist/shell/index.css.map +1 -1
- package/dist/shell/index.d.ts +43 -7
- package/dist/shell/index.js +1570 -259
- 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 +1 -1
- package/dist/test/index.js +4 -4
- package/dist/vite/index.js +501 -106
- package/dist/vite/index.js.map +1 -1
- package/package.json +13 -8
- 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-BKPDKUDN.js.map +0 -1
- package/dist/chunk-FZNABWJC.js +0 -282
- package/dist/chunk-FZNABWJC.js.map +0 -1
- package/dist/chunk-JTCUIJPZ.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;
|
|
@@ -853,13 +855,13 @@ function createTracer(read) {
|
|
|
853
855
|
}
|
|
854
856
|
return modules.get(file) ?? null;
|
|
855
857
|
};
|
|
856
|
-
return (
|
|
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
|
|
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,
|
|
905
|
-
const trace = tracer(
|
|
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,
|
|
933
|
-
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);
|
|
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 = {
|
|
1165
|
-
|
|
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",
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
|
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(
|
|
1390
|
-
const
|
|
1391
|
-
if (
|
|
1392
|
-
depsSent =
|
|
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,
|
|
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, () =>
|
|
1510
|
-
}
|
|
1903
|
+
capture = servePages(server, base, () => shellUnder(root));
|
|
1904
|
+
},
|
|
1905
|
+
closeServer: () => capture?.close()
|
|
1511
1906
|
}
|
|
1512
1907
|
];
|
|
1513
1908
|
}
|