@open-wxa/wxa-runner-core 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -39,9 +39,135 @@ __export(src_exports, {
39
39
  module.exports = __toCommonJS(src_exports);
40
40
 
41
41
  // src/server/DevServer.ts
42
- var import_crypto3 = require("crypto");
42
+ var import_crypto4 = require("crypto");
43
+
44
+ // src/server/FileStore.ts
45
+ var import_crypto = require("crypto");
46
+ var FileStore = class {
47
+ /** @param roots 预先创建的目录(如用户目录)。rmdir 不能删除它们和它们的上级目录:删掉之后再写 USER_DATA_PATH 都会失败 */
48
+ constructor(roots = []) {
49
+ this.etagPrefix = (0, import_crypto.randomUUID)();
50
+ this.version = 0;
51
+ this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" }]]);
52
+ this.roots = roots.map((root) => this.normalize(root));
53
+ for (const root of this.roots) this.mkdir(root, true);
54
+ }
55
+ normalize(filePath) {
56
+ const stripped = String(filePath || "").replace(/^[a-z]+:\/\/[^/]*/i, "");
57
+ const parts = [];
58
+ for (const part of stripped.split("/")) {
59
+ if (!part || part === ".") continue;
60
+ if (part === "..") parts.pop();
61
+ else parts.push(part);
62
+ }
63
+ return "/" + parts.join("/");
64
+ }
65
+ stat(filePath) {
66
+ return this.nodes.get(this.normalize(filePath));
67
+ }
68
+ mkdir(dirPath, recursive = false) {
69
+ const target = this.normalize(dirPath);
70
+ if (this.nodes.has(target)) throw new Error(`file already exists ${dirPath}`);
71
+ const parent = target.slice(0, target.lastIndexOf("/")) || "/";
72
+ if (!this.nodes.get(parent)?.dir) {
73
+ if (!recursive) throw new Error(`no such file or directory ${dirPath}`);
74
+ this.mkdir(parent, true);
75
+ }
76
+ this.nodes.set(target, { dir: true, data: Buffer.alloc(0), mtime: Date.now(), etag: "" });
77
+ }
78
+ write(filePath, input, append = false) {
79
+ const data = Buffer.from(input);
80
+ const target = this.normalize(filePath);
81
+ const parent = target.slice(0, target.lastIndexOf("/")) || "/";
82
+ if (!this.nodes.get(parent)?.dir) throw new Error(`no such file or directory ${filePath}`);
83
+ const existing = this.nodes.get(target);
84
+ if (existing?.dir) throw new Error(`illegal operation on a directory ${filePath}`);
85
+ const contents = append && existing ? Buffer.concat([existing.data, data]) : data;
86
+ this.nodes.set(target, { dir: false, data: contents, mtime: Date.now(), etag: `${this.etagPrefix}-${++this.version}` });
87
+ }
88
+ read(filePath) {
89
+ const node = this.stat(filePath);
90
+ if (!node) throw new Error(`no such file or directory, open ${filePath}`);
91
+ if (node.dir) throw new Error(`illegal operation on a directory ${filePath}`);
92
+ return node.data;
93
+ }
94
+ readdir(dirPath) {
95
+ const target = this.normalize(dirPath);
96
+ if (!this.nodes.get(target)?.dir) throw new Error(`no such file or directory ${dirPath}`);
97
+ const prefix = target === "/" ? "/" : target + "/";
98
+ return [...this.nodes.keys()].filter((key) => key.startsWith(prefix) && key !== target && !key.slice(prefix.length).includes("/")).map((key) => key.slice(prefix.length));
99
+ }
100
+ remove(filePath, options) {
101
+ const target = this.normalize(filePath);
102
+ const node = this.nodes.get(target);
103
+ if (!node) throw new Error(`no such file or directory ${filePath}`);
104
+ if (node.dir !== options.dir) throw new Error(options.dir ? `not a directory ${filePath}` : `operation not permitted ${filePath}`);
105
+ if (node.dir) {
106
+ const children = this.readdir(target);
107
+ if (children.length && !options.recursive) throw new Error(`directory not empty ${filePath}`);
108
+ for (const key of [...this.nodes.keys()]) if (key.startsWith(target + "/")) this.nodes.delete(key);
109
+ }
110
+ this.nodes.delete(target);
111
+ }
112
+ execute(operation, args) {
113
+ switch (operation) {
114
+ case "readFile": {
115
+ const data = this.read(args.filePath);
116
+ return args.encoding === void 0 ? { data: data.toString("base64"), binary: true } : { data: data.toString(args.encoding) };
117
+ }
118
+ case "writeFile":
119
+ case "appendFile": {
120
+ const data = Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8");
121
+ this.write(args.filePath, data, operation === "appendFile");
122
+ return {};
123
+ }
124
+ case "copyFile":
125
+ this.write(args.destPath, this.read(args.srcPath));
126
+ return {};
127
+ case "access":
128
+ if (!this.stat(args.path)) throw new Error(`no such file or directory ${args.path}`);
129
+ return {};
130
+ case "stat": {
131
+ const node = this.stat(args.path);
132
+ if (!node) throw new Error(`no such file or directory ${args.path}`);
133
+ const stats = {
134
+ mode: node.dir ? 16877 : 33188,
135
+ size: node.data.length,
136
+ lastAccessedTime: Math.floor(node.mtime / 1e3),
137
+ lastModifiedTime: Math.floor(node.mtime / 1e3)
138
+ };
139
+ return { stats, ...stats };
140
+ }
141
+ case "getFileInfo":
142
+ return { size: this.read(args.filePath).length };
143
+ case "mkdir":
144
+ this.mkdir(args.dirPath, !!args.recursive);
145
+ return {};
146
+ case "readdir":
147
+ return { files: this.readdir(args.dirPath) };
148
+ case "unlink":
149
+ this.remove(args.filePath, { dir: false });
150
+ return {};
151
+ case "rmdir": {
152
+ const target = this.normalize(args.dirPath);
153
+ const prefix = target === "/" ? "/" : target + "/";
154
+ if (this.roots.some((root) => root === target || root.startsWith(prefix))) {
155
+ throw new Error(`operation not permitted ${args.dirPath}`);
156
+ }
157
+ this.remove(args.dirPath, { dir: true, recursive: !!args.recursive });
158
+ return {};
159
+ }
160
+ default:
161
+ throw new Error(`unsupported filesystem operation ${operation}`);
162
+ }
163
+ }
164
+ };
165
+
166
+ // src/server/DevServer.ts
43
167
  var import_fs4 = __toESM(require("fs"));
44
168
  var import_http = __toESM(require("http"));
169
+ var import_net = __toESM(require("net"));
170
+ var import_os = __toESM(require("os"));
45
171
  var import_path4 = __toESM(require("path"));
46
172
  var import_express2 = __toESM(require("express"));
47
173
  var import_ws2 = require("ws");
@@ -481,6 +607,22 @@ function readJsonFile(file) {
481
607
  }
482
608
  }
483
609
 
610
+ // src/runtime/networkPanel.ts
611
+ var PROXY_PREFIX = "/__wxa__/proxy/";
612
+ var MOCK_PREFIX = "/__wxa__/mock/";
613
+ function fromServerPath(prefix, serverPath) {
614
+ if (!serverPath.startsWith(prefix)) return null;
615
+ const match = /^(https?)\/([^/?#]+)(.*)$/.exec(serverPath.slice(prefix.length));
616
+ if (!match) return null;
617
+ const url = `${match[1]}://${match[2]}${match[3]}`;
618
+ try {
619
+ new URL(url);
620
+ return url;
621
+ } catch {
622
+ return null;
623
+ }
624
+ }
625
+
484
626
  // src/server/plugins.ts
485
627
  var import_wxa_runner_extractor2 = require("@open-wxa/wxa-runner-extractor");
486
628
 
@@ -758,7 +900,7 @@ ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</
758
900
  }
759
901
  function workerScript(build2, baseLib) {
760
902
  const modules = build2.workerModules.map((id) => `/__wxa__/module/${encodeURI(id)}?v=${build2.version}`);
761
- const prelude = `var __wxConfig = ${toJs(build2.wxConfig)};
903
+ const prelude = `var __wxConfig = ${toJs(build2.wxConfig)}; __wxConfig.env.USER_DATA_PATH = location.origin + "/__wxa__/usr/" + new URL(location.href).searchParams.get("session");
762
904
  importScripts('/__wxa__/runtime/worker.js');
763
905
  `;
764
906
  if (!baseLib.hasWorker) {
@@ -840,13 +982,62 @@ function vlq(value) {
840
982
  return out;
841
983
  }
842
984
 
985
+ // src/server/forwardConsole.ts
986
+ var MAX_LENGTH = 4e3;
987
+ var LIMIT_PER_SECOND = 20;
988
+ var CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
989
+ function printBrowserReports(socket, logger) {
990
+ let windowStart = 0;
991
+ let printed = 0;
992
+ let dropped = 0;
993
+ socket.on("message", (data) => {
994
+ const report = parseReport(data);
995
+ if (!report) return;
996
+ const now = Date.now();
997
+ if (now - windowStart >= 1e3) {
998
+ windowStart = now;
999
+ printed = 0;
1000
+ }
1001
+ if (printed >= LIMIT_PER_SECOND) {
1002
+ if (dropped++ === 0) {
1003
+ setTimeout(() => {
1004
+ logger.warn(`[wxa-runner] \u6D4F\u89C8\u5668 1 \u79D2\u5185\u8F6C\u53D1\u7684\u62A5\u9519\u592A\u591A\uFF0C\u7701\u7565\u4E86 ${dropped} \u6761`);
1005
+ dropped = 0;
1006
+ }, windowStart + 1e3 - now).unref();
1007
+ }
1008
+ return;
1009
+ }
1010
+ printed++;
1011
+ logger[report.level](`[wxa-runner] ${clean(report.text)}`);
1012
+ });
1013
+ }
1014
+ function parseReport(data) {
1015
+ try {
1016
+ const report = JSON.parse(data.toString());
1017
+ if (report?.type === "console" && (report.level === "warn" || report.level === "error") && typeof report.text === "string") return report;
1018
+ } catch {
1019
+ }
1020
+ return null;
1021
+ }
1022
+ function clean(text) {
1023
+ const printable = text.replace(CONTROL_CHARS, "");
1024
+ return printable.length > MAX_LENGTH ? `${printable.slice(0, MAX_LENGTH)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF09` : printable;
1025
+ }
1026
+ function ignoreConsoleHook(sourceMap) {
1027
+ const map = JSON.parse(sourceMap);
1028
+ map.x_google_ignoreList = map.sources.flatMap((source, index) => source.endsWith("consoleHook.ts") ? [index] : []);
1029
+ return JSON.stringify(map);
1030
+ }
1031
+
843
1032
  // src/server/proxy.ts
1033
+ var import_node_stream = require("stream");
1034
+ var import_promises = require("stream/promises");
844
1035
  var DROPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding", "keep-alive", "upgrade", "expect"]);
845
1036
  var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection", "set-cookie"]);
846
1037
  async function handleProxy(req, res) {
847
- const target = String(req.query.url || "");
848
- if (!/^https?:\/\//i.test(target)) {
849
- res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${target}"`);
1038
+ const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1039
+ if (!target) {
1040
+ res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
850
1041
  return;
851
1042
  }
852
1043
  let requestHeaders = {};
@@ -868,31 +1059,47 @@ async function handleProxy(req, res) {
868
1059
  headers["content-type"] = browserContentType;
869
1060
  }
870
1061
  const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
871
- let upstream;
1062
+ const controller = new AbortController();
1063
+ const cancel = () => controller.abort();
1064
+ res.once("close", cancel);
872
1065
  try {
873
- upstream = await fetch(target, {
874
- method: req.method,
875
- headers,
876
- body: hasBody ? req.body : void 0,
877
- redirect: "follow"
1066
+ let upstream;
1067
+ try {
1068
+ upstream = await fetch(target, {
1069
+ method: req.method,
1070
+ headers,
1071
+ body: hasBody ? req.body : void 0,
1072
+ redirect: "follow",
1073
+ signal: controller.signal
1074
+ });
1075
+ } catch (error) {
1076
+ if (res.destroyed || controller.signal.aborted) return;
1077
+ const cause = error?.cause?.message;
1078
+ res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1079
+ return;
1080
+ }
1081
+ const responseHeaders = {};
1082
+ upstream.headers.forEach((value, key) => {
1083
+ responseHeaders[key] = value;
878
1084
  });
1085
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1086
+ res.status(upstream.status);
1087
+ upstream.headers.forEach((value, key) => {
1088
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
1089
+ });
1090
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1091
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1092
+ res.flushHeaders();
1093
+ if (!upstream.body) {
1094
+ res.end();
1095
+ return;
1096
+ }
1097
+ await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(upstream.body), res);
879
1098
  } catch (error) {
880
- const cause = error?.cause?.message;
881
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
882
- return;
1099
+ if (!res.destroyed) res.destroy(error);
1100
+ } finally {
1101
+ res.off("close", cancel);
883
1102
  }
884
- const responseHeaders = {};
885
- upstream.headers.forEach((value, key) => {
886
- responseHeaders[key] = value;
887
- });
888
- const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
889
- res.status(upstream.status);
890
- upstream.headers.forEach((value, key) => {
891
- if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
892
- });
893
- res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
894
- res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
895
- res.send(Buffer.from(await upstream.arrayBuffer()));
896
1103
  }
897
1104
 
898
1105
  // src/server/hmr.ts
@@ -910,8 +1117,8 @@ function planHotUpdate(files, units) {
910
1117
 
911
1118
  // src/server/developerTicket.ts
912
1119
  var import_child_process = require("child_process");
913
- var import_crypto = require("crypto");
914
- var import_promises = require("timers/promises");
1120
+ var import_crypto2 = require("crypto");
1121
+ var import_promises2 = require("timers/promises");
915
1122
  var import_ws = require("ws");
916
1123
  var import_wxa_runner_extractor3 = require("@open-wxa/wxa-runner-extractor");
917
1124
  var AUTO_PORT = 9420;
@@ -960,7 +1167,7 @@ async function requestTicketOnceWindowReady(port, projectPath) {
960
1167
  } catch (error) {
961
1168
  if (!(error instanceof WindowNotFound)) throw error;
962
1169
  if (Date.now() >= deadline) throw new Error(`\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u627E\u4E0D\u5230\u9879\u76EE\u7A97\u53E3\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u6253\u5F00\u9879\u76EE ${projectPath}`);
963
- await (0, import_promises.setTimeout)(WINDOW_RETRY_INTERVAL);
1170
+ await (0, import_promises2.setTimeout)(WINDOW_RETRY_INTERVAL);
964
1171
  }
965
1172
  }
966
1173
  }
@@ -1002,7 +1209,7 @@ function cliErrorMessage(stdout) {
1002
1209
  }
1003
1210
  function requestTicket(port) {
1004
1211
  return new Promise((resolve, reject) => {
1005
- const id = `wxa-runner-${(0, import_crypto.randomUUID)()}`;
1212
+ const id = `wxa-runner-${(0, import_crypto2.randomUUID)()}`;
1006
1213
  const socket = new import_ws.WebSocket(`ws://127.0.0.1:${port}`);
1007
1214
  const settle = (finish) => {
1008
1215
  clearTimeout(timer);
@@ -1153,7 +1360,7 @@ function builtinSetupModule() {
1153
1360
  }
1154
1361
 
1155
1362
  // src/server/watcher.ts
1156
- var import_crypto2 = require("crypto");
1363
+ var import_crypto3 = require("crypto");
1157
1364
  var import_fs3 = __toESM(require("fs"));
1158
1365
  var import_path3 = __toESM(require("path"));
1159
1366
  var import_chokidar = __toESM(require("chokidar"));
@@ -1197,7 +1404,7 @@ function updateFingerprint(fingerprints, file, event) {
1197
1404
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1198
1405
  let next;
1199
1406
  try {
1200
- next = (0, import_crypto2.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1407
+ next = (0, import_crypto3.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1201
1408
  } catch {
1202
1409
  return false;
1203
1410
  }
@@ -1224,8 +1431,9 @@ var DevServer = class {
1224
1431
  this.sockets = null;
1225
1432
  this.watcher = null;
1226
1433
  this.address = "";
1434
+ this.lanAccess = false;
1227
1435
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1228
- this.tempFiles = /* @__PURE__ */ new Map();
1436
+ this.fileSessions = /* @__PURE__ */ new Map();
1229
1437
  this.logger = options.logger || console;
1230
1438
  this.project = new MiniProgram(import_path4.default.resolve(options.projectPath));
1231
1439
  this.baseLib = (0, import_wxa_runner_extractor5.resolveBaseLib)({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
@@ -1244,9 +1452,16 @@ var DevServer = class {
1244
1452
  this.setupEnv = collectSetupEnv(process.env);
1245
1453
  this.app = this.createApp();
1246
1454
  }
1455
+ /** 本机访问的地址 */
1247
1456
  get url() {
1248
1457
  return this.address;
1249
1458
  }
1459
+ /** 局域网里其他设备访问的地址,每块网卡一个;只监听本机时为空 */
1460
+ get networkUrls() {
1461
+ if (!this.lanAccess) return [];
1462
+ const { port } = new URL(this.address);
1463
+ return Object.values(import_os.default.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1464
+ }
1250
1465
  async start() {
1251
1466
  try {
1252
1467
  const build2 = await this.builder.build();
@@ -1256,7 +1471,11 @@ var DevServer = class {
1256
1471
  ${error.message}`);
1257
1472
  }
1258
1473
  const server = import_http.default.createServer(this.app);
1259
- this.sockets = new import_ws2.WebSocketServer({ noServer: true });
1474
+ this.sockets = new import_ws2.WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
1475
+ this.sockets.on("connection", (socket) => {
1476
+ socket.on("error", () => void 0);
1477
+ if (this.options.forwardConsole) printBrowserReports(socket, this.logger);
1478
+ });
1260
1479
  server.on("upgrade", (req, socket, head) => {
1261
1480
  if (req.url?.startsWith("/__wxa__/ws")) {
1262
1481
  this.sockets.handleUpgrade(req, socket, head, (ws) => this.sockets.emit("connection", ws, req));
@@ -1265,13 +1484,10 @@ ${error.message}`);
1265
1484
  }
1266
1485
  });
1267
1486
  const host = this.options.host || "localhost";
1268
- await new Promise((resolve, reject) => {
1269
- server.once("error", reject);
1270
- server.listen(this.options.port ?? 3e3, host, () => resolve());
1271
- });
1487
+ const port = await listen(server, this.options.port, host, this.logger);
1272
1488
  this.server = server;
1273
- const { port } = server.address();
1274
- this.address = `http://${host}:${port}`;
1489
+ this.lanAccess = LOCAL_HOST_OF_ANY_ADDRESS.has(host);
1490
+ this.address = `http://${LOCAL_HOST_OF_ANY_ADDRESS.get(host) ?? host}:${port}`;
1275
1491
  if (this.options.watch !== false) this.startWatching();
1276
1492
  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}`));
1277
1493
  return this.address;
@@ -1286,6 +1502,7 @@ ${error.message}`);
1286
1502
  server.closeAllConnections();
1287
1503
  await new Promise((resolve) => server.close(() => resolve()));
1288
1504
  }
1505
+ this.fileSessions.clear();
1289
1506
  }
1290
1507
  createApp() {
1291
1508
  const app = (0, import_express2.default)();
@@ -1300,7 +1517,13 @@ ${error.message}`);
1300
1517
  });
1301
1518
  app.get("/__wxa__/app-info.json", async (_req, res) => {
1302
1519
  noStore(res);
1303
- const base = { appid: this.project.appid, baseLibVersion: this.baseLib.version, device: this.device, devtoolsLogin: this.devtoolsLogin };
1520
+ const base = {
1521
+ appid: this.project.appid,
1522
+ baseLibVersion: this.baseLib.version,
1523
+ device: this.device,
1524
+ devtoolsLogin: this.devtoolsLogin,
1525
+ forwardConsole: this.options.forwardConsole ?? false
1526
+ };
1304
1527
  try {
1305
1528
  const build2 = await this.plugins.refresh(await this.builder.build());
1306
1529
  const info = { ...base, version: build2.version, wxConfig: build2.wxConfig, pages: build2.pages };
@@ -1379,7 +1602,18 @@ ${error.message}`);
1379
1602
  app.get("/__wxa__/page-frame.js", this.withBuild((build2, _req, res) => {
1380
1603
  res.type("js").send(build2.pageFrame);
1381
1604
  }));
1382
- app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), { cacheControl: false }));
1605
+ app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1606
+ const file = import_path4.default.join(this.runtimeDir(), "frame.js.map");
1607
+ if (!import_fs4.default.existsSync(file)) return next();
1608
+ res.type("json").send(ignoreConsoleHook(import_fs4.default.readFileSync(file, "utf8")));
1609
+ });
1610
+ app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), {
1611
+ cacheControl: false,
1612
+ // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1613
+ setHeaders: (res, file) => {
1614
+ if (import_path4.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1615
+ }
1616
+ }));
1383
1617
  const sourceMaps = /* @__PURE__ */ new Map();
1384
1618
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1385
1619
  const file = import_path4.default.resolve(this.baseLib.dir, req.params[0]);
@@ -1393,16 +1627,45 @@ ${error.message}`);
1393
1627
  if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path4.default.basename(file)}.map`);
1394
1628
  }
1395
1629
  }));
1396
- app.all("/__wxa__/proxy", import_express2.default.raw({ type: () => true, limit: "100mb" }), handleProxy);
1397
- app.post("/__wxa__/tmp", import_express2.default.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1630
+ app.use(PROXY_PREFIX, import_express2.default.raw({ type: () => true, limit: "100mb" }), handleProxy);
1631
+ app.use(MOCK_PREFIX, (_req, res) => {
1632
+ 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");
1633
+ });
1634
+ const sessionFiles = (session) => {
1635
+ if (!/^[a-z0-9-]{1,80}$/i.test(session)) return null;
1636
+ let files = this.fileSessions.get(session);
1637
+ if (!files) {
1638
+ files = new FileStore([`/__wxa__/usr/${session}`, `/__wxa__/tmp/${session}`]);
1639
+ this.fileSessions.set(session, files);
1640
+ }
1641
+ return files;
1642
+ };
1643
+ app.post("/__wxa__/fs/:session", import_express2.default.json({ limit: "150mb" }), (req, res) => {
1644
+ const files = sessionFiles(req.params.session);
1645
+ if (!files) {
1646
+ res.status(400).json({ error: `invalid filesystem session ${req.params.session}` });
1647
+ return;
1648
+ }
1649
+ try {
1650
+ res.json({ result: files.execute(req.body.operation, req.body.args || {}) });
1651
+ } catch (error) {
1652
+ res.json({ error: error.message });
1653
+ }
1654
+ });
1655
+ app.post("/__wxa__/tmp/:session", import_express2.default.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1398
1656
  const type = String(req.query.type || "png");
1399
1657
  if (!/^[a-z0-9]+$/i.test(type)) {
1400
1658
  res.status(400).type("text").send(`\u975E\u6CD5\u7684\u6587\u4EF6\u7C7B\u578B ${type}`);
1401
1659
  return;
1402
1660
  }
1403
- const name = `${(0, import_crypto3.randomUUID)()}.${type}`;
1404
- this.tempFiles.set(name, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1405
- res.json({ url: `${req.protocol}://${req.get("host")}/__wxa__/tmp/${name}` });
1661
+ const files = sessionFiles(req.params.session);
1662
+ if (!files) {
1663
+ res.status(400).type("text").send(`\u975E\u6CD5\u7684\u4F1A\u8BDD\u6807\u8BC6 ${req.params.session}`);
1664
+ return;
1665
+ }
1666
+ const filePath = `/__wxa__/tmp/${req.params.session}/${(0, import_crypto4.randomUUID)()}.${type}`;
1667
+ files.write(filePath, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1668
+ res.json({ url: `${req.protocol}://${req.get("host")}${filePath}` });
1406
1669
  });
1407
1670
  app.get("/__wxa__/region/:kind", (req, res) => {
1408
1671
  const kind = req.params.kind;
@@ -1413,14 +1676,16 @@ ${error.message}`);
1413
1676
  }
1414
1677
  res.type("text/plain; charset=utf-8").set("Cache-Control", "public, max-age=86400").send(data);
1415
1678
  });
1416
- app.get("/__wxa__/tmp/:name", (req, res) => {
1417
- const data = this.tempFiles.get(req.params.name);
1418
- if (!data) {
1679
+ app.get("/__wxa__/:kind(usr|tmp)/:session/*", (req, res) => {
1680
+ const files = this.fileSessions.get(req.params.session);
1681
+ const node = files?.stat(`/__wxa__/${req.params.kind}/${req.params.session}/` + req.params[0]);
1682
+ if (!node || node.dir) {
1419
1683
  res.status(404).end();
1420
1684
  return;
1421
1685
  }
1422
- res.type(import_path4.default.extname(req.params.name));
1423
- res.setHeader("ETag", `"${req.params.name}"`);
1686
+ const data = node.data;
1687
+ res.type(import_path4.default.extname(req.params[0]));
1688
+ res.setHeader("ETag", `"${node.etag}"`);
1424
1689
  res.setHeader("Accept-Ranges", "bytes");
1425
1690
  const ranges = req.range(data.length);
1426
1691
  if (ranges === -1) {
@@ -1436,8 +1701,15 @@ ${error.message}`);
1436
1701
  }
1437
1702
  res.send(data);
1438
1703
  });
1439
- app.delete("/__wxa__/tmp/:name", (req, res) => {
1440
- res.status(this.tempFiles.delete(req.params.name) ? 204 : 404).end();
1704
+ app.delete("/__wxa__/tmp/:session/*", (req, res) => {
1705
+ try {
1706
+ const files = this.fileSessions.get(req.params.session);
1707
+ if (!files) throw new Error("file not exist");
1708
+ files.remove(`/__wxa__/tmp/${req.params.session}/` + req.params[0], { dir: false });
1709
+ res.status(204).end();
1710
+ } catch {
1711
+ res.status(404).end();
1712
+ }
1441
1713
  });
1442
1714
  if (this.ticketSource) app.use("/__wxa__/api", devtoolsLoginRouter(this.project.appid, this.ticketSource));
1443
1715
  app.use("/__wxa__/webview", import_express2.default.static(root));
@@ -1526,6 +1798,38 @@ ${message}`);
1526
1798
  });
1527
1799
  }
1528
1800
  };
1801
+ var DEFAULT_PORT = 3e3;
1802
+ var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1803
+ async function listen(server, port, host, logger) {
1804
+ for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1805
+ const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1806
+ if (!taken) {
1807
+ const actual = server.address().port;
1808
+ if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
1809
+ return actual;
1810
+ }
1811
+ 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`);
1812
+ }
1813
+ }
1814
+ function bind(server, port, host) {
1815
+ return new Promise((resolve, reject) => {
1816
+ const onError = (error) => error.code === "EADDRINUSE" ? resolve(false) : reject(error);
1817
+ server.once("error", onError);
1818
+ server.listen(port, host, () => {
1819
+ server.off("error", onError);
1820
+ resolve(true);
1821
+ });
1822
+ });
1823
+ }
1824
+ async function takenOnLoopback(port) {
1825
+ for (const address of ["127.0.0.1", "::1"]) {
1826
+ const probe = import_net.default.createServer();
1827
+ const free = await bind(probe, port, address).catch(() => true);
1828
+ if (!free) return true;
1829
+ await new Promise((resolve) => probe.close(resolve));
1830
+ }
1831
+ return false;
1832
+ }
1529
1833
  function resolveSetupFile(option, projectPath) {
1530
1834
  if (option === false) return null;
1531
1835
  if (!option) return findSetupFile(projectPath);