@open-wxa/wxa-runner-core 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3,9 +3,135 @@ import {
3
3
  } from "./chunk-NGXYLLYS.mjs";
4
4
 
5
5
  // src/server/DevServer.ts
6
- import { randomUUID as randomUUID2 } from "crypto";
6
+ import { randomUUID as randomUUID3 } from "crypto";
7
+
8
+ // src/server/FileStore.ts
9
+ import { randomUUID } from "crypto";
10
+ var FileStore = class {
11
+ /** @param roots 预先创建的目录(如用户目录)。rmdir 不能删除它们和它们的上级目录:删掉之后再写 USER_DATA_PATH 都会失败 */
12
+ constructor(roots = []) {
13
+ this.etagPrefix = randomUUID();
14
+ this.version = 0;
15
+ this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" }]]);
16
+ this.roots = roots.map((root) => this.normalize(root));
17
+ for (const root of this.roots) this.mkdir(root, true);
18
+ }
19
+ normalize(filePath) {
20
+ const stripped = String(filePath || "").replace(/^[a-z]+:\/\/[^/]*/i, "");
21
+ const parts = [];
22
+ for (const part of stripped.split("/")) {
23
+ if (!part || part === ".") continue;
24
+ if (part === "..") parts.pop();
25
+ else parts.push(part);
26
+ }
27
+ return "/" + parts.join("/");
28
+ }
29
+ stat(filePath) {
30
+ return this.nodes.get(this.normalize(filePath));
31
+ }
32
+ mkdir(dirPath, recursive = false) {
33
+ const target = this.normalize(dirPath);
34
+ if (this.nodes.has(target)) throw new Error(`file already exists ${dirPath}`);
35
+ const parent = target.slice(0, target.lastIndexOf("/")) || "/";
36
+ if (!this.nodes.get(parent)?.dir) {
37
+ if (!recursive) throw new Error(`no such file or directory ${dirPath}`);
38
+ this.mkdir(parent, true);
39
+ }
40
+ this.nodes.set(target, { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" });
41
+ }
42
+ write(filePath, input, append = false) {
43
+ const data = Buffer.from(input);
44
+ const target = this.normalize(filePath);
45
+ const parent = target.slice(0, target.lastIndexOf("/")) || "/";
46
+ if (!this.nodes.get(parent)?.dir) throw new Error(`no such file or directory ${filePath}`);
47
+ const existing = this.nodes.get(target);
48
+ if (existing?.dir) throw new Error(`illegal operation on a directory ${filePath}`);
49
+ const contents = append && existing ? Buffer.concat([existing.data, data]) : data;
50
+ this.nodes.set(target, { dir: false, data: contents, mtime: Date.now(), etag: `${this.etagPrefix}-${++this.version}` });
51
+ }
52
+ read(filePath) {
53
+ const node = this.stat(filePath);
54
+ if (!node) throw new Error(`no such file or directory, open ${filePath}`);
55
+ if (node.dir) throw new Error(`illegal operation on a directory ${filePath}`);
56
+ return node.data;
57
+ }
58
+ readdir(dirPath) {
59
+ const target = this.normalize(dirPath);
60
+ if (!this.nodes.get(target)?.dir) throw new Error(`no such file or directory ${dirPath}`);
61
+ const prefix = target === "/" ? "/" : target + "/";
62
+ return [...this.nodes.keys()].filter((key) => key.startsWith(prefix) && key !== target && !key.slice(prefix.length).includes("/")).map((key) => key.slice(prefix.length));
63
+ }
64
+ remove(filePath, options) {
65
+ const target = this.normalize(filePath);
66
+ const node = this.nodes.get(target);
67
+ if (!node) throw new Error(`no such file or directory ${filePath}`);
68
+ if (node.dir !== options.dir) throw new Error(options.dir ? `not a directory ${filePath}` : `operation not permitted ${filePath}`);
69
+ if (node.dir) {
70
+ const children = this.readdir(target);
71
+ if (children.length && !options.recursive) throw new Error(`directory not empty ${filePath}`);
72
+ for (const key of [...this.nodes.keys()]) if (key.startsWith(target + "/")) this.nodes.delete(key);
73
+ }
74
+ this.nodes.delete(target);
75
+ }
76
+ execute(operation, args) {
77
+ switch (operation) {
78
+ case "readFile": {
79
+ const data = this.read(args.filePath);
80
+ return args.encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(args.encoding) };
81
+ }
82
+ case "writeFile":
83
+ case "appendFile": {
84
+ const data = Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8");
85
+ this.write(args.filePath, data, operation === "appendFile");
86
+ return {};
87
+ }
88
+ case "copyFile":
89
+ this.write(args.destPath, this.read(args.srcPath));
90
+ return {};
91
+ case "access":
92
+ if (!this.stat(args.path)) throw new Error(`no such file or directory ${args.path}`);
93
+ return {};
94
+ case "stat": {
95
+ const node = this.stat(args.path);
96
+ if (!node) throw new Error(`no such file or directory ${args.path}`);
97
+ const stats = {
98
+ mode: node.dir ? 16877 : 33188,
99
+ size: node.data.length,
100
+ lastAccessedTime: Math.floor(node.mtime / 1e3),
101
+ lastModifiedTime: Math.floor(node.mtime / 1e3)
102
+ };
103
+ return { stats, ...stats };
104
+ }
105
+ case "getFileInfo":
106
+ return { size: this.read(args.filePath).length };
107
+ case "mkdir":
108
+ this.mkdir(args.dirPath, !!args.recursive);
109
+ return {};
110
+ case "readdir":
111
+ return { files: this.readdir(args.dirPath) };
112
+ case "unlink":
113
+ this.remove(args.filePath, { dir: false });
114
+ return {};
115
+ case "rmdir": {
116
+ const target = this.normalize(args.dirPath);
117
+ const prefix = target === "/" ? "/" : target + "/";
118
+ if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
119
+ throw new Error(`operation not permitted ${args.dirPath}`);
120
+ }
121
+ this.remove(args.dirPath, { dir: true, recursive: !!args.recursive });
122
+ return {};
123
+ }
124
+ default:
125
+ throw new Error(`unsupported filesystem operation ${operation}`);
126
+ }
127
+ }
128
+ };
129
+
130
+ // src/server/DevServer.ts
7
131
  import fs4 from "fs";
8
132
  import http from "http";
133
+ import net from "net";
134
+ import os from "os";
9
135
  import path4 from "path";
10
136
  import express2 from "express";
11
137
  import { WebSocket as WebSocket2, WebSocketServer } from "ws";
@@ -338,15 +464,32 @@ var MiniProgram = class {
338
464
  }
339
465
  return pages;
340
466
  }
341
- /** 读取并预处理页面/组件 json,id 不带扩展名 */
467
+ /**
468
+ * 读取并预处理页面/组件 json,id 不带扩展名。
469
+ * 基础库运行时只把组件引用和当前路径拼在一起,不查找 miniprogram_npm、不补 /index(开发者工具编译时已解析好),
470
+ * 所以能解析到的引用都换成组件的绝对路径
471
+ */
342
472
  readUnitJson(id, app) {
343
473
  const json = { ...readJsonFile(path.join(this.root, id + ".json")) };
344
- json.usingComponents = { ...json.usingComponents };
345
- if (!id.startsWith("miniprogram_npm/") && !this.isInIndependentSubPackage(id, app)) {
474
+ const resolve = (fromId, ref) => {
475
+ const resolved = this.resolveComponent(fromId, ref);
476
+ return resolved ? "/" + resolved : null;
477
+ };
478
+ json.usingComponents = Object.fromEntries(
479
+ Object.entries(json.usingComponents || {}).map(([tag, ref]) => [tag, resolve(id, ref) ?? ref])
480
+ );
481
+ if (json.componentGenerics) {
482
+ json.componentGenerics = Object.fromEntries(Object.entries(json.componentGenerics).map(([name, generic]) => [
483
+ name,
484
+ typeof generic === "object" && generic.default ? { ...generic, default: resolve(id, generic.default) ?? generic.default } : generic
485
+ ]));
486
+ }
487
+ if (!/(^|\/)miniprogram_npm\//.test(id) && !this.isInIndependentSubPackage(id, app)) {
346
488
  for (const [tag, ref] of Object.entries(app.usingComponents || {})) {
347
489
  if (json.usingComponents[tag]) continue;
348
- if (ref.startsWith("/") || ref.startsWith("plugin://")) {
349
- json.usingComponents[tag] = ref;
490
+ const resolved = resolve("app", ref);
491
+ if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
492
+ json.usingComponents[tag] = resolved ?? ref;
350
493
  } else {
351
494
  const relative = path.posix.relative(path.posix.dirname(id), ref);
352
495
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
@@ -445,6 +588,22 @@ function readJsonFile(file) {
445
588
  }
446
589
  }
447
590
 
591
+ // src/runtime/networkPanel.ts
592
+ var PROXY_PREFIX = "/__wxa__/proxy/";
593
+ var MOCK_PREFIX = "/__wxa__/mock/";
594
+ function fromServerPath(prefix, serverPath) {
595
+ if (!serverPath.startsWith(prefix)) return null;
596
+ const match = /^(https?)\/([^/?#]+)(.*)$/.exec(serverPath.slice(prefix.length));
597
+ if (!match) return null;
598
+ const url = `${match[1]}://${match[2]}${match[3]}`;
599
+ try {
600
+ new URL(url);
601
+ return url;
602
+ } catch {
603
+ return null;
604
+ }
605
+ }
606
+
448
607
  // src/server/plugins.ts
449
608
  import { readWxvpkgFile, readWxvpkgIndex } from "@open-wxa/wxa-runner-extractor";
450
609
 
@@ -722,7 +881,7 @@ ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</
722
881
  }
723
882
  function workerScript(build2, baseLib) {
724
883
  const modules = build2.workerModules.map((id) => `/__wxa__/module/${encodeURI(id)}?v=${build2.version}`);
725
- const prelude = `var __wxConfig = ${toJs(build2.wxConfig)};
884
+ const prelude = `var __wxConfig = ${toJs(build2.wxConfig)}; __wxConfig.env.USER_DATA_PATH = location.origin + "/__wxa__/usr/" + new URL(location.href).searchParams.get("session");
726
885
  importScripts('/__wxa__/runtime/worker.js');
727
886
  `;
728
887
  if (!baseLib.hasWorker) {
@@ -804,13 +963,62 @@ function vlq(value) {
804
963
  return out;
805
964
  }
806
965
 
966
+ // src/server/forwardConsole.ts
967
+ var MAX_LENGTH = 4e3;
968
+ var LIMIT_PER_SECOND = 20;
969
+ var CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
970
+ function printBrowserReports(socket, logger) {
971
+ let windowStart = 0;
972
+ let printed = 0;
973
+ let dropped = 0;
974
+ socket.on("message", (data) => {
975
+ const report = parseReport(data);
976
+ if (!report) return;
977
+ const now = Date.now();
978
+ if (now - windowStart >= 1e3) {
979
+ windowStart = now;
980
+ printed = 0;
981
+ }
982
+ if (printed >= LIMIT_PER_SECOND) {
983
+ if (dropped++ === 0) {
984
+ setTimeout(() => {
985
+ logger.warn(`[wxa-runner] \u6D4F\u89C8\u5668 1 \u79D2\u5185\u8F6C\u53D1\u7684\u62A5\u9519\u592A\u591A\uFF0C\u7701\u7565\u4E86 ${dropped} \u6761`);
986
+ dropped = 0;
987
+ }, windowStart + 1e3 - now).unref();
988
+ }
989
+ return;
990
+ }
991
+ printed++;
992
+ logger[report.level](`[wxa-runner] ${clean(report.text)}`);
993
+ });
994
+ }
995
+ function parseReport(data) {
996
+ try {
997
+ const report = JSON.parse(data.toString());
998
+ if (report?.type === "console" && (report.level === "warn" || report.level === "error") && typeof report.text === "string") return report;
999
+ } catch {
1000
+ }
1001
+ return null;
1002
+ }
1003
+ function clean(text) {
1004
+ const printable = text.replace(CONTROL_CHARS, "");
1005
+ return printable.length > MAX_LENGTH ? `${printable.slice(0, MAX_LENGTH)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF09` : printable;
1006
+ }
1007
+ function ignoreConsoleHook(sourceMap) {
1008
+ const map = JSON.parse(sourceMap);
1009
+ map.x_google_ignoreList = map.sources.flatMap((source, index) => source.endsWith("consoleHook.ts") ? [index] : []);
1010
+ return JSON.stringify(map);
1011
+ }
1012
+
807
1013
  // src/server/proxy.ts
1014
+ import { Readable } from "stream";
1015
+ import { pipeline } from "stream/promises";
808
1016
  var DROPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding", "keep-alive", "upgrade", "expect"]);
809
1017
  var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection", "set-cookie"]);
810
1018
  async function handleProxy(req, res) {
811
- const target = String(req.query.url || "");
812
- if (!/^https?:\/\//i.test(target)) {
813
- res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${target}"`);
1019
+ const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1020
+ if (!target) {
1021
+ res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
814
1022
  return;
815
1023
  }
816
1024
  let requestHeaders = {};
@@ -832,31 +1040,47 @@ async function handleProxy(req, res) {
832
1040
  headers["content-type"] = browserContentType;
833
1041
  }
834
1042
  const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
835
- let upstream;
1043
+ const controller = new AbortController();
1044
+ const cancel = () => controller.abort();
1045
+ res.once("close", cancel);
836
1046
  try {
837
- upstream = await fetch(target, {
838
- method: req.method,
839
- headers,
840
- body: hasBody ? req.body : void 0,
841
- redirect: "follow"
1047
+ let upstream;
1048
+ try {
1049
+ upstream = await fetch(target, {
1050
+ method: req.method,
1051
+ headers,
1052
+ body: hasBody ? req.body : void 0,
1053
+ redirect: "follow",
1054
+ signal: controller.signal
1055
+ });
1056
+ } catch (error) {
1057
+ if (res.destroyed || controller.signal.aborted) return;
1058
+ const cause = error?.cause?.message;
1059
+ res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1060
+ return;
1061
+ }
1062
+ const responseHeaders = {};
1063
+ upstream.headers.forEach((value, key) => {
1064
+ responseHeaders[key] = value;
842
1065
  });
1066
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1067
+ res.status(upstream.status);
1068
+ upstream.headers.forEach((value, key) => {
1069
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1070
+ });
1071
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1072
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1073
+ res.flushHeaders();
1074
+ if (!upstream.body) {
1075
+ res.end();
1076
+ return;
1077
+ }
1078
+ await pipeline(Readable.fromWeb(upstream.body), res);
843
1079
  } catch (error) {
844
- const cause = error?.cause?.message;
845
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
846
- return;
1080
+ if (!res.destroyed) res.destroy(error);
1081
+ } finally {
1082
+ res.off("close", cancel);
847
1083
  }
848
- const responseHeaders = {};
849
- upstream.headers.forEach((value, key) => {
850
- responseHeaders[key] = value;
851
- });
852
- const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
853
- res.status(upstream.status);
854
- upstream.headers.forEach((value, key) => {
855
- if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
856
- });
857
- res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
858
- res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
859
- res.send(Buffer.from(await upstream.arrayBuffer()));
860
1084
  }
861
1085
 
862
1086
  // src/server/hmr.ts
@@ -874,7 +1098,7 @@ function planHotUpdate(files, units) {
874
1098
 
875
1099
  // src/server/developerTicket.ts
876
1100
  import { execFile } from "child_process";
877
- import { randomUUID } from "crypto";
1101
+ import { randomUUID as randomUUID2 } from "crypto";
878
1102
  import { setTimeout as sleep } from "timers/promises";
879
1103
  import { WebSocket } from "ws";
880
1104
  import { findDeveloperTicket, findElectronDevtoolsCli, TICKET_ENV as TICKET_ENV2 } from "@open-wxa/wxa-runner-extractor";
@@ -966,7 +1190,7 @@ function cliErrorMessage(stdout) {
966
1190
  }
967
1191
  function requestTicket(port) {
968
1192
  return new Promise((resolve, reject) => {
969
- const id = `wxa-runner-${randomUUID()}`;
1193
+ const id = `wxa-runner-${randomUUID2()}`;
970
1194
  const socket = new WebSocket(`ws://127.0.0.1:${port}`);
971
1195
  const settle = (finish) => {
972
1196
  clearTimeout(timer);
@@ -1188,8 +1412,9 @@ var DevServer = class {
1188
1412
  this.sockets = null;
1189
1413
  this.watcher = null;
1190
1414
  this.address = "";
1415
+ this.lanAccess = false;
1191
1416
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1192
- this.tempFiles = /* @__PURE__ */ new Map();
1417
+ this.fileSessions = /* @__PURE__ */ new Map();
1193
1418
  this.logger = options.logger || console;
1194
1419
  this.project = new MiniProgram(path4.resolve(options.projectPath));
1195
1420
  this.baseLib = resolveBaseLib({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
@@ -1208,9 +1433,16 @@ var DevServer = class {
1208
1433
  this.setupEnv = collectSetupEnv(process.env);
1209
1434
  this.app = this.createApp();
1210
1435
  }
1436
+ /** 本机访问的地址 */
1211
1437
  get url() {
1212
1438
  return this.address;
1213
1439
  }
1440
+ /** 局域网里其他设备访问的地址,每块网卡一个;只监听本机时为空 */
1441
+ get networkUrls() {
1442
+ if (!this.lanAccess) return [];
1443
+ const { port } = new URL(this.address);
1444
+ return Object.values(os.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1445
+ }
1214
1446
  async start() {
1215
1447
  try {
1216
1448
  const build2 = await this.builder.build();
@@ -1220,7 +1452,11 @@ var DevServer = class {
1220
1452
  ${error.message}`);
1221
1453
  }
1222
1454
  const server = http.createServer(this.app);
1223
- this.sockets = new WebSocketServer({ noServer: true });
1455
+ this.sockets = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
1456
+ this.sockets.on("connection", (socket) => {
1457
+ socket.on("error", () => void 0);
1458
+ if (this.options.forwardConsole) printBrowserReports(socket, this.logger);
1459
+ });
1224
1460
  server.on("upgrade", (req, socket, head) => {
1225
1461
  if (req.url?.startsWith("/__wxa__/ws")) {
1226
1462
  this.sockets.handleUpgrade(req, socket, head, (ws) => this.sockets.emit("connection", ws, req));
@@ -1229,13 +1465,10 @@ ${error.message}`);
1229
1465
  }
1230
1466
  });
1231
1467
  const host = this.options.host || "localhost";
1232
- await new Promise((resolve, reject) => {
1233
- server.once("error", reject);
1234
- server.listen(this.options.port ?? 3e3, host, () => resolve());
1235
- });
1468
+ const port = await listen(server, this.options.port, host, this.logger);
1236
1469
  this.server = server;
1237
- const { port } = server.address();
1238
- this.address = `http://${host}:${port}`;
1470
+ this.lanAccess = LOCAL_HOST_OF_ANY_ADDRESS.has(host);
1471
+ this.address = `http://${LOCAL_HOST_OF_ANY_ADDRESS.get(host) ?? host}:${port}`;
1239
1472
  if (this.options.watch !== false) this.startWatching();
1240
1473
  this.ticketSource?.().then(requireUsableTicket).catch((error) => this.logger.warn(`[wxa-runner] \u5F00\u53D1\u8005\u5DE5\u5177\u7684\u767B\u5F55\u6001\u6682\u4E0D\u53EF\u7528\uFF08--devtools-login\uFF09\uFF1A${error.message}`));
1241
1474
  return this.address;
@@ -1250,6 +1483,7 @@ ${error.message}`);
1250
1483
  server.closeAllConnections();
1251
1484
  await new Promise((resolve) => server.close(() => resolve()));
1252
1485
  }
1486
+ this.fileSessions.clear();
1253
1487
  }
1254
1488
  createApp() {
1255
1489
  const app = express2();
@@ -1264,7 +1498,13 @@ ${error.message}`);
1264
1498
  });
1265
1499
  app.get("/__wxa__/app-info.json", async (_req, res) => {
1266
1500
  noStore(res);
1267
- const base = { appid: this.project.appid, baseLibVersion: this.baseLib.version, device: this.device, devtoolsLogin: this.devtoolsLogin };
1501
+ const base = {
1502
+ appid: this.project.appid,
1503
+ baseLibVersion: this.baseLib.version,
1504
+ device: this.device,
1505
+ devtoolsLogin: this.devtoolsLogin,
1506
+ forwardConsole: this.options.forwardConsole ?? false
1507
+ };
1268
1508
  try {
1269
1509
  const build2 = await this.plugins.refresh(await this.builder.build());
1270
1510
  const info = { ...base, version: build2.version, wxConfig: build2.wxConfig, pages: build2.pages };
@@ -1343,7 +1583,18 @@ ${error.message}`);
1343
1583
  app.get("/__wxa__/page-frame.js", this.withBuild((build2, _req, res) => {
1344
1584
  res.type("js").send(build2.pageFrame);
1345
1585
  }));
1346
- app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), { cacheControl: false }));
1586
+ app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1587
+ const file = path4.join(this.runtimeDir(), "frame.js.map");
1588
+ if (!fs4.existsSync(file)) return next();
1589
+ res.type("json").send(ignoreConsoleHook(fs4.readFileSync(file, "utf8")));
1590
+ });
1591
+ app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), {
1592
+ cacheControl: false,
1593
+ // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1594
+ setHeaders: (res, file) => {
1595
+ if (path4.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1596
+ }
1597
+ }));
1347
1598
  const sourceMaps = /* @__PURE__ */ new Map();
1348
1599
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1349
1600
  const file = path4.resolve(this.baseLib.dir, req.params[0]);
@@ -1357,16 +1608,45 @@ ${error.message}`);
1357
1608
  if (file.endsWith(".js")) res.setHeader("SourceMap", `${path4.basename(file)}.map`);
1358
1609
  }
1359
1610
  }));
1360
- app.all("/__wxa__/proxy", express2.raw({ type: () => true, limit: "100mb" }), handleProxy);
1361
- app.post("/__wxa__/tmp", express2.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1611
+ app.use(PROXY_PREFIX, express2.raw({ type: () => true, limit: "100mb" }), handleProxy);
1612
+ app.use(MOCK_PREFIX, (_req, res) => {
1613
+ res.status(503).type("text").send("\u8FD9\u4E2A\u8BF7\u6C42\u5DF2\u88AB\u62E6\u622A\u89C4\u5219\u62E6\u4E0B\uFF0C\u8FD9\u91CC\u53EA\u7528\u4E8E\u5728 Network \u9762\u677F\u91CC\u663E\u793A\u5B83\u3002Service Worker \u6CA1\u6709\u751F\u6548\uFF08\u5982 DevTools \u52FE\u9009\u4E86 Bypass for network\uFF09\uFF0C\u770B\u4E0D\u5230\u62E6\u622A\u89C4\u5219\u7ED9\u51FA\u7684\u54CD\u5E94");
1614
+ });
1615
+ const sessionFiles = (session) => {
1616
+ if (!/^[a-z0-9-]{1,80}$/i.test(session)) return null;
1617
+ let files = this.fileSessions.get(session);
1618
+ if (!files) {
1619
+ files = new FileStore([`/__wxa__/usr/${session}`, `/__wxa__/tmp/${session}`]);
1620
+ this.fileSessions.set(session, files);
1621
+ }
1622
+ return files;
1623
+ };
1624
+ app.post("/__wxa__/fs/:session", express2.json({ limit: "150mb" }), (req, res) => {
1625
+ const files = sessionFiles(req.params.session);
1626
+ if (!files) {
1627
+ res.status(400).json({ error: `invalid filesystem session ${req.params.session}` });
1628
+ return;
1629
+ }
1630
+ try {
1631
+ res.json({ result: files.execute(req.body.operation, req.body.args || {}) });
1632
+ } catch (error) {
1633
+ res.json({ error: error.message });
1634
+ }
1635
+ });
1636
+ app.post("/__wxa__/tmp/:session", express2.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1362
1637
  const type = String(req.query.type || "png");
1363
1638
  if (!/^[a-z0-9]+$/i.test(type)) {
1364
1639
  res.status(400).type("text").send(`\u975E\u6CD5\u7684\u6587\u4EF6\u7C7B\u578B ${type}`);
1365
1640
  return;
1366
1641
  }
1367
- const name = `${randomUUID2()}.${type}`;
1368
- this.tempFiles.set(name, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1369
- res.json({ url: `${req.protocol}://${req.get("host")}/__wxa__/tmp/${name}` });
1642
+ const files = sessionFiles(req.params.session);
1643
+ if (!files) {
1644
+ res.status(400).type("text").send(`\u975E\u6CD5\u7684\u4F1A\u8BDD\u6807\u8BC6 ${req.params.session}`);
1645
+ return;
1646
+ }
1647
+ const filePath = `/__wxa__/tmp/${req.params.session}/${randomUUID3()}.${type}`;
1648
+ files.write(filePath, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1649
+ res.json({ url: `${req.protocol}://${req.get("host")}${filePath}` });
1370
1650
  });
1371
1651
  app.get("/__wxa__/region/:kind", (req, res) => {
1372
1652
  const kind = req.params.kind;
@@ -1377,14 +1657,16 @@ ${error.message}`);
1377
1657
  }
1378
1658
  res.type("text/plain; charset=utf-8").set("Cache-Control", "public, max-age=86400").send(data);
1379
1659
  });
1380
- app.get("/__wxa__/tmp/:name", (req, res) => {
1381
- const data = this.tempFiles.get(req.params.name);
1382
- if (!data) {
1660
+ app.get("/__wxa__/:kind(usr|tmp)/:session/*", (req, res) => {
1661
+ const files = this.fileSessions.get(req.params.session);
1662
+ const node = files?.stat(`/__wxa__/${req.params.kind}/${req.params.session}/` + req.params[0]);
1663
+ if (!node || node.dir) {
1383
1664
  res.status(404).end();
1384
1665
  return;
1385
1666
  }
1386
- res.type(path4.extname(req.params.name));
1387
- res.setHeader("ETag", `"${req.params.name}"`);
1667
+ const data = node.data;
1668
+ res.type(path4.extname(req.params[0]));
1669
+ res.setHeader("ETag", `"${node.etag}"`);
1388
1670
  res.setHeader("Accept-Ranges", "bytes");
1389
1671
  const ranges = req.range(data.length);
1390
1672
  if (ranges === -1) {
@@ -1400,8 +1682,15 @@ ${error.message}`);
1400
1682
  }
1401
1683
  res.send(data);
1402
1684
  });
1403
- app.delete("/__wxa__/tmp/:name", (req, res) => {
1404
- res.status(this.tempFiles.delete(req.params.name) ? 204 : 404).end();
1685
+ app.delete("/__wxa__/tmp/:session/*", (req, res) => {
1686
+ try {
1687
+ const files = this.fileSessions.get(req.params.session);
1688
+ if (!files) throw new Error("file not exist");
1689
+ files.remove(`/__wxa__/tmp/${req.params.session}/` + req.params[0], { dir: false });
1690
+ res.status(204).end();
1691
+ } catch {
1692
+ res.status(404).end();
1693
+ }
1405
1694
  });
1406
1695
  if (this.ticketSource) app.use("/__wxa__/api", devtoolsLoginRouter(this.project.appid, this.ticketSource));
1407
1696
  app.use("/__wxa__/webview", express2.static(root));
@@ -1490,6 +1779,38 @@ ${message}`);
1490
1779
  });
1491
1780
  }
1492
1781
  };
1782
+ var DEFAULT_PORT = 3e3;
1783
+ var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1784
+ async function listen(server, port, host, logger) {
1785
+ for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1786
+ const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1787
+ if (!taken) {
1788
+ const actual = server.address().port;
1789
+ if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
1790
+ return actual;
1791
+ }
1792
+ if (port !== void 0) throw new Error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5360\u7528\uFF1A\u5148\u5173\u95ED\u5360\u7528\u5B83\u7684\u8FDB\u7A0B\uFF0C\u6216\u7528 --port \u6362\u4E00\u4E2A\u7AEF\u53E3`);
1793
+ }
1794
+ }
1795
+ function bind(server, port, host) {
1796
+ return new Promise((resolve, reject) => {
1797
+ const onError = (error) => error.code === "EADDRINUSE" ? resolve(false) : reject(error);
1798
+ server.once("error", onError);
1799
+ server.listen(port, host, () => {
1800
+ server.off("error", onError);
1801
+ resolve(true);
1802
+ });
1803
+ });
1804
+ }
1805
+ async function takenOnLoopback(port) {
1806
+ for (const address of ["127.0.0.1", "::1"]) {
1807
+ const probe = net.createServer();
1808
+ const free = await bind(probe, port, address).catch(() => true);
1809
+ if (!free) return true;
1810
+ await new Promise((resolve) => probe.close(resolve));
1811
+ }
1812
+ return false;
1813
+ }
1493
1814
  function resolveSetupFile(option, projectPath) {
1494
1815
  if (option === false) return null;
1495
1816
  if (!option) return findSetupFile(projectPath);