@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.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";
@@ -445,6 +571,22 @@ function readJsonFile(file) {
445
571
  }
446
572
  }
447
573
 
574
+ // src/runtime/networkPanel.ts
575
+ var PROXY_PREFIX = "/__wxa__/proxy/";
576
+ var MOCK_PREFIX = "/__wxa__/mock/";
577
+ function fromServerPath(prefix, serverPath) {
578
+ if (!serverPath.startsWith(prefix)) return null;
579
+ const match = /^(https?)\/([^/?#]+)(.*)$/.exec(serverPath.slice(prefix.length));
580
+ if (!match) return null;
581
+ const url = `${match[1]}://${match[2]}${match[3]}`;
582
+ try {
583
+ new URL(url);
584
+ return url;
585
+ } catch {
586
+ return null;
587
+ }
588
+ }
589
+
448
590
  // src/server/plugins.ts
449
591
  import { readWxvpkgFile, readWxvpkgIndex } from "@open-wxa/wxa-runner-extractor";
450
592
 
@@ -722,7 +864,7 @@ ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</
722
864
  }
723
865
  function workerScript(build2, baseLib) {
724
866
  const modules = build2.workerModules.map((id) => `/__wxa__/module/${encodeURI(id)}?v=${build2.version}`);
725
- const prelude = `var __wxConfig = ${toJs(build2.wxConfig)};
867
+ const prelude = `var __wxConfig = ${toJs(build2.wxConfig)}; __wxConfig.env.USER_DATA_PATH = location.origin + "/__wxa__/usr/" + new URL(location.href).searchParams.get("session");
726
868
  importScripts('/__wxa__/runtime/worker.js');
727
869
  `;
728
870
  if (!baseLib.hasWorker) {
@@ -804,13 +946,62 @@ function vlq(value) {
804
946
  return out;
805
947
  }
806
948
 
949
+ // src/server/forwardConsole.ts
950
+ var MAX_LENGTH = 4e3;
951
+ var LIMIT_PER_SECOND = 20;
952
+ var CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
953
+ function printBrowserReports(socket, logger) {
954
+ let windowStart = 0;
955
+ let printed = 0;
956
+ let dropped = 0;
957
+ socket.on("message", (data) => {
958
+ const report = parseReport(data);
959
+ if (!report) return;
960
+ const now = Date.now();
961
+ if (now - windowStart >= 1e3) {
962
+ windowStart = now;
963
+ printed = 0;
964
+ }
965
+ if (printed >= LIMIT_PER_SECOND) {
966
+ if (dropped++ === 0) {
967
+ setTimeout(() => {
968
+ logger.warn(`[wxa-runner] \u6D4F\u89C8\u5668 1 \u79D2\u5185\u8F6C\u53D1\u7684\u62A5\u9519\u592A\u591A\uFF0C\u7701\u7565\u4E86 ${dropped} \u6761`);
969
+ dropped = 0;
970
+ }, windowStart + 1e3 - now).unref();
971
+ }
972
+ return;
973
+ }
974
+ printed++;
975
+ logger[report.level](`[wxa-runner] ${clean(report.text)}`);
976
+ });
977
+ }
978
+ function parseReport(data) {
979
+ try {
980
+ const report = JSON.parse(data.toString());
981
+ if (report?.type === "console" && (report.level === "warn" || report.level === "error") && typeof report.text === "string") return report;
982
+ } catch {
983
+ }
984
+ return null;
985
+ }
986
+ function clean(text) {
987
+ const printable = text.replace(CONTROL_CHARS, "");
988
+ return printable.length > MAX_LENGTH ? `${printable.slice(0, MAX_LENGTH)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF09` : printable;
989
+ }
990
+ function ignoreConsoleHook(sourceMap) {
991
+ const map = JSON.parse(sourceMap);
992
+ map.x_google_ignoreList = map.sources.flatMap((source, index) => source.endsWith("consoleHook.ts") ? [index] : []);
993
+ return JSON.stringify(map);
994
+ }
995
+
807
996
  // src/server/proxy.ts
997
+ import { Readable } from "stream";
998
+ import { pipeline } from "stream/promises";
808
999
  var DROPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding", "keep-alive", "upgrade", "expect"]);
809
1000
  var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection", "set-cookie"]);
810
1001
  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}"`);
1002
+ const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1003
+ if (!target) {
1004
+ res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
814
1005
  return;
815
1006
  }
816
1007
  let requestHeaders = {};
@@ -832,31 +1023,47 @@ async function handleProxy(req, res) {
832
1023
  headers["content-type"] = browserContentType;
833
1024
  }
834
1025
  const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
835
- let upstream;
1026
+ const controller = new AbortController();
1027
+ const cancel = () => controller.abort();
1028
+ res.once("close", cancel);
836
1029
  try {
837
- upstream = await fetch(target, {
838
- method: req.method,
839
- headers,
840
- body: hasBody ? req.body : void 0,
841
- redirect: "follow"
1030
+ let upstream;
1031
+ try {
1032
+ upstream = await fetch(target, {
1033
+ method: req.method,
1034
+ headers,
1035
+ body: hasBody ? req.body : void 0,
1036
+ redirect: "follow",
1037
+ signal: controller.signal
1038
+ });
1039
+ } catch (error) {
1040
+ if (res.destroyed || controller.signal.aborted) return;
1041
+ const cause = error?.cause?.message;
1042
+ res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1043
+ return;
1044
+ }
1045
+ const responseHeaders = {};
1046
+ upstream.headers.forEach((value, key) => {
1047
+ responseHeaders[key] = value;
1048
+ });
1049
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1050
+ res.status(upstream.status);
1051
+ upstream.headers.forEach((value, key) => {
1052
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
842
1053
  });
1054
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1055
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1056
+ res.flushHeaders();
1057
+ if (!upstream.body) {
1058
+ res.end();
1059
+ return;
1060
+ }
1061
+ await pipeline(Readable.fromWeb(upstream.body), res);
843
1062
  } catch (error) {
844
- const cause = error?.cause?.message;
845
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
846
- return;
1063
+ if (!res.destroyed) res.destroy(error);
1064
+ } finally {
1065
+ res.off("close", cancel);
847
1066
  }
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
1067
  }
861
1068
 
862
1069
  // src/server/hmr.ts
@@ -874,7 +1081,7 @@ function planHotUpdate(files, units) {
874
1081
 
875
1082
  // src/server/developerTicket.ts
876
1083
  import { execFile } from "child_process";
877
- import { randomUUID } from "crypto";
1084
+ import { randomUUID as randomUUID2 } from "crypto";
878
1085
  import { setTimeout as sleep } from "timers/promises";
879
1086
  import { WebSocket } from "ws";
880
1087
  import { findDeveloperTicket, findElectronDevtoolsCli, TICKET_ENV as TICKET_ENV2 } from "@open-wxa/wxa-runner-extractor";
@@ -966,7 +1173,7 @@ function cliErrorMessage(stdout) {
966
1173
  }
967
1174
  function requestTicket(port) {
968
1175
  return new Promise((resolve, reject) => {
969
- const id = `wxa-runner-${randomUUID()}`;
1176
+ const id = `wxa-runner-${randomUUID2()}`;
970
1177
  const socket = new WebSocket(`ws://127.0.0.1:${port}`);
971
1178
  const settle = (finish) => {
972
1179
  clearTimeout(timer);
@@ -1188,8 +1395,9 @@ var DevServer = class {
1188
1395
  this.sockets = null;
1189
1396
  this.watcher = null;
1190
1397
  this.address = "";
1398
+ this.lanAccess = false;
1191
1399
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1192
- this.tempFiles = /* @__PURE__ */ new Map();
1400
+ this.fileSessions = /* @__PURE__ */ new Map();
1193
1401
  this.logger = options.logger || console;
1194
1402
  this.project = new MiniProgram(path4.resolve(options.projectPath));
1195
1403
  this.baseLib = resolveBaseLib({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
@@ -1208,9 +1416,16 @@ var DevServer = class {
1208
1416
  this.setupEnv = collectSetupEnv(process.env);
1209
1417
  this.app = this.createApp();
1210
1418
  }
1419
+ /** 本机访问的地址 */
1211
1420
  get url() {
1212
1421
  return this.address;
1213
1422
  }
1423
+ /** 局域网里其他设备访问的地址,每块网卡一个;只监听本机时为空 */
1424
+ get networkUrls() {
1425
+ if (!this.lanAccess) return [];
1426
+ const { port } = new URL(this.address);
1427
+ return Object.values(os.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1428
+ }
1214
1429
  async start() {
1215
1430
  try {
1216
1431
  const build2 = await this.builder.build();
@@ -1220,7 +1435,11 @@ var DevServer = class {
1220
1435
  ${error.message}`);
1221
1436
  }
1222
1437
  const server = http.createServer(this.app);
1223
- this.sockets = new WebSocketServer({ noServer: true });
1438
+ this.sockets = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
1439
+ this.sockets.on("connection", (socket) => {
1440
+ socket.on("error", () => void 0);
1441
+ if (this.options.forwardConsole) printBrowserReports(socket, this.logger);
1442
+ });
1224
1443
  server.on("upgrade", (req, socket, head) => {
1225
1444
  if (req.url?.startsWith("/__wxa__/ws")) {
1226
1445
  this.sockets.handleUpgrade(req, socket, head, (ws) => this.sockets.emit("connection", ws, req));
@@ -1229,13 +1448,10 @@ ${error.message}`);
1229
1448
  }
1230
1449
  });
1231
1450
  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
- });
1451
+ const port = await listen(server, this.options.port, host, this.logger);
1236
1452
  this.server = server;
1237
- const { port } = server.address();
1238
- this.address = `http://${host}:${port}`;
1453
+ this.lanAccess = LOCAL_HOST_OF_ANY_ADDRESS.has(host);
1454
+ this.address = `http://${LOCAL_HOST_OF_ANY_ADDRESS.get(host) ?? host}:${port}`;
1239
1455
  if (this.options.watch !== false) this.startWatching();
1240
1456
  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
1457
  return this.address;
@@ -1250,6 +1466,7 @@ ${error.message}`);
1250
1466
  server.closeAllConnections();
1251
1467
  await new Promise((resolve) => server.close(() => resolve()));
1252
1468
  }
1469
+ this.fileSessions.clear();
1253
1470
  }
1254
1471
  createApp() {
1255
1472
  const app = express2();
@@ -1264,7 +1481,13 @@ ${error.message}`);
1264
1481
  });
1265
1482
  app.get("/__wxa__/app-info.json", async (_req, res) => {
1266
1483
  noStore(res);
1267
- const base = { appid: this.project.appid, baseLibVersion: this.baseLib.version, device: this.device, devtoolsLogin: this.devtoolsLogin };
1484
+ const base = {
1485
+ appid: this.project.appid,
1486
+ baseLibVersion: this.baseLib.version,
1487
+ device: this.device,
1488
+ devtoolsLogin: this.devtoolsLogin,
1489
+ forwardConsole: this.options.forwardConsole ?? false
1490
+ };
1268
1491
  try {
1269
1492
  const build2 = await this.plugins.refresh(await this.builder.build());
1270
1493
  const info = { ...base, version: build2.version, wxConfig: build2.wxConfig, pages: build2.pages };
@@ -1343,7 +1566,18 @@ ${error.message}`);
1343
1566
  app.get("/__wxa__/page-frame.js", this.withBuild((build2, _req, res) => {
1344
1567
  res.type("js").send(build2.pageFrame);
1345
1568
  }));
1346
- app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), { cacheControl: false }));
1569
+ app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1570
+ const file = path4.join(this.runtimeDir(), "frame.js.map");
1571
+ if (!fs4.existsSync(file)) return next();
1572
+ res.type("json").send(ignoreConsoleHook(fs4.readFileSync(file, "utf8")));
1573
+ });
1574
+ app.use("/__wxa__/runtime", express2.static(this.runtimeDir(), {
1575
+ cacheControl: false,
1576
+ // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1577
+ setHeaders: (res, file) => {
1578
+ if (path4.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1579
+ }
1580
+ }));
1347
1581
  const sourceMaps = /* @__PURE__ */ new Map();
1348
1582
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1349
1583
  const file = path4.resolve(this.baseLib.dir, req.params[0]);
@@ -1357,16 +1591,45 @@ ${error.message}`);
1357
1591
  if (file.endsWith(".js")) res.setHeader("SourceMap", `${path4.basename(file)}.map`);
1358
1592
  }
1359
1593
  }));
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) => {
1594
+ app.use(PROXY_PREFIX, express2.raw({ type: () => true, limit: "100mb" }), handleProxy);
1595
+ app.use(MOCK_PREFIX, (_req, res) => {
1596
+ 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");
1597
+ });
1598
+ const sessionFiles = (session) => {
1599
+ if (!/^[a-z0-9-]{1,80}$/i.test(session)) return null;
1600
+ let files = this.fileSessions.get(session);
1601
+ if (!files) {
1602
+ files = new FileStore([`/__wxa__/usr/${session}`, `/__wxa__/tmp/${session}`]);
1603
+ this.fileSessions.set(session, files);
1604
+ }
1605
+ return files;
1606
+ };
1607
+ app.post("/__wxa__/fs/:session", express2.json({ limit: "150mb" }), (req, res) => {
1608
+ const files = sessionFiles(req.params.session);
1609
+ if (!files) {
1610
+ res.status(400).json({ error: `invalid filesystem session ${req.params.session}` });
1611
+ return;
1612
+ }
1613
+ try {
1614
+ res.json({ result: files.execute(req.body.operation, req.body.args || {}) });
1615
+ } catch (error) {
1616
+ res.json({ error: error.message });
1617
+ }
1618
+ });
1619
+ app.post("/__wxa__/tmp/:session", express2.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1362
1620
  const type = String(req.query.type || "png");
1363
1621
  if (!/^[a-z0-9]+$/i.test(type)) {
1364
1622
  res.status(400).type("text").send(`\u975E\u6CD5\u7684\u6587\u4EF6\u7C7B\u578B ${type}`);
1365
1623
  return;
1366
1624
  }
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}` });
1625
+ const files = sessionFiles(req.params.session);
1626
+ if (!files) {
1627
+ res.status(400).type("text").send(`\u975E\u6CD5\u7684\u4F1A\u8BDD\u6807\u8BC6 ${req.params.session}`);
1628
+ return;
1629
+ }
1630
+ const filePath = `/__wxa__/tmp/${req.params.session}/${randomUUID3()}.${type}`;
1631
+ files.write(filePath, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1632
+ res.json({ url: `${req.protocol}://${req.get("host")}${filePath}` });
1370
1633
  });
1371
1634
  app.get("/__wxa__/region/:kind", (req, res) => {
1372
1635
  const kind = req.params.kind;
@@ -1377,14 +1640,16 @@ ${error.message}`);
1377
1640
  }
1378
1641
  res.type("text/plain; charset=utf-8").set("Cache-Control", "public, max-age=86400").send(data);
1379
1642
  });
1380
- app.get("/__wxa__/tmp/:name", (req, res) => {
1381
- const data = this.tempFiles.get(req.params.name);
1382
- if (!data) {
1643
+ app.get("/__wxa__/:kind(usr|tmp)/:session/*", (req, res) => {
1644
+ const files = this.fileSessions.get(req.params.session);
1645
+ const node = files?.stat(`/__wxa__/${req.params.kind}/${req.params.session}/` + req.params[0]);
1646
+ if (!node || node.dir) {
1383
1647
  res.status(404).end();
1384
1648
  return;
1385
1649
  }
1386
- res.type(path4.extname(req.params.name));
1387
- res.setHeader("ETag", `"${req.params.name}"`);
1650
+ const data = node.data;
1651
+ res.type(path4.extname(req.params[0]));
1652
+ res.setHeader("ETag", `"${node.etag}"`);
1388
1653
  res.setHeader("Accept-Ranges", "bytes");
1389
1654
  const ranges = req.range(data.length);
1390
1655
  if (ranges === -1) {
@@ -1400,8 +1665,15 @@ ${error.message}`);
1400
1665
  }
1401
1666
  res.send(data);
1402
1667
  });
1403
- app.delete("/__wxa__/tmp/:name", (req, res) => {
1404
- res.status(this.tempFiles.delete(req.params.name) ? 204 : 404).end();
1668
+ app.delete("/__wxa__/tmp/:session/*", (req, res) => {
1669
+ try {
1670
+ const files = this.fileSessions.get(req.params.session);
1671
+ if (!files) throw new Error("file not exist");
1672
+ files.remove(`/__wxa__/tmp/${req.params.session}/` + req.params[0], { dir: false });
1673
+ res.status(204).end();
1674
+ } catch {
1675
+ res.status(404).end();
1676
+ }
1405
1677
  });
1406
1678
  if (this.ticketSource) app.use("/__wxa__/api", devtoolsLoginRouter(this.project.appid, this.ticketSource));
1407
1679
  app.use("/__wxa__/webview", express2.static(root));
@@ -1490,6 +1762,38 @@ ${message}`);
1490
1762
  });
1491
1763
  }
1492
1764
  };
1765
+ var DEFAULT_PORT = 3e3;
1766
+ var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1767
+ async function listen(server, port, host, logger) {
1768
+ for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1769
+ const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1770
+ if (!taken) {
1771
+ const actual = server.address().port;
1772
+ if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
1773
+ return actual;
1774
+ }
1775
+ 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`);
1776
+ }
1777
+ }
1778
+ function bind(server, port, host) {
1779
+ return new Promise((resolve, reject) => {
1780
+ const onError = (error) => error.code === "EADDRINUSE" ? resolve(false) : reject(error);
1781
+ server.once("error", onError);
1782
+ server.listen(port, host, () => {
1783
+ server.off("error", onError);
1784
+ resolve(true);
1785
+ });
1786
+ });
1787
+ }
1788
+ async function takenOnLoopback(port) {
1789
+ for (const address of ["127.0.0.1", "::1"]) {
1790
+ const probe = net.createServer();
1791
+ const free = await bind(probe, port, address).catch(() => true);
1792
+ if (!free) return true;
1793
+ await new Promise((resolve) => probe.close(resolve));
1794
+ }
1795
+ return false;
1796
+ }
1493
1797
  function resolveSetupFile(option, projectPath) {
1494
1798
  if (option === false) return null;
1495
1799
  if (!option) return findSetupFile(projectPath);