@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.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");
@@ -374,15 +500,32 @@ var MiniProgram = class {
374
500
  }
375
501
  return pages;
376
502
  }
377
- /** 读取并预处理页面/组件 json,id 不带扩展名 */
503
+ /**
504
+ * 读取并预处理页面/组件 json,id 不带扩展名。
505
+ * 基础库运行时只把组件引用和当前路径拼在一起,不查找 miniprogram_npm、不补 /index(开发者工具编译时已解析好),
506
+ * 所以能解析到的引用都换成组件的绝对路径
507
+ */
378
508
  readUnitJson(id, app) {
379
509
  const json = { ...readJsonFile(import_path.default.join(this.root, id + ".json")) };
380
- json.usingComponents = { ...json.usingComponents };
381
- if (!id.startsWith("miniprogram_npm/") && !this.isInIndependentSubPackage(id, app)) {
510
+ const resolve = (fromId, ref) => {
511
+ const resolved = this.resolveComponent(fromId, ref);
512
+ return resolved ? "/" + resolved : null;
513
+ };
514
+ json.usingComponents = Object.fromEntries(
515
+ Object.entries(json.usingComponents || {}).map(([tag, ref]) => [tag, resolve(id, ref) ?? ref])
516
+ );
517
+ if (json.componentGenerics) {
518
+ json.componentGenerics = Object.fromEntries(Object.entries(json.componentGenerics).map(([name, generic]) => [
519
+ name,
520
+ typeof generic === "object" && generic.default ? { ...generic, default: resolve(id, generic.default) ?? generic.default } : generic
521
+ ]));
522
+ }
523
+ if (!/(^|\/)miniprogram_npm\//.test(id) && !this.isInIndependentSubPackage(id, app)) {
382
524
  for (const [tag, ref] of Object.entries(app.usingComponents || {})) {
383
525
  if (json.usingComponents[tag]) continue;
384
- if (ref.startsWith("/") || ref.startsWith("plugin://")) {
385
- json.usingComponents[tag] = ref;
526
+ const resolved = resolve("app", ref);
527
+ if (resolved || ref.startsWith("/") || /^plugin(-private)?:\/\//.test(ref)) {
528
+ json.usingComponents[tag] = resolved ?? ref;
386
529
  } else {
387
530
  const relative = import_path.default.posix.relative(import_path.default.posix.dirname(id), ref);
388
531
  json.usingComponents[tag] = relative.startsWith(".") ? relative : "./" + relative;
@@ -481,6 +624,22 @@ function readJsonFile(file) {
481
624
  }
482
625
  }
483
626
 
627
+ // src/runtime/networkPanel.ts
628
+ var PROXY_PREFIX = "/__wxa__/proxy/";
629
+ var MOCK_PREFIX = "/__wxa__/mock/";
630
+ function fromServerPath(prefix, serverPath) {
631
+ if (!serverPath.startsWith(prefix)) return null;
632
+ const match = /^(https?)\/([^/?#]+)(.*)$/.exec(serverPath.slice(prefix.length));
633
+ if (!match) return null;
634
+ const url = `${match[1]}://${match[2]}${match[3]}`;
635
+ try {
636
+ new URL(url);
637
+ return url;
638
+ } catch {
639
+ return null;
640
+ }
641
+ }
642
+
484
643
  // src/server/plugins.ts
485
644
  var import_wxa_runner_extractor2 = require("@open-wxa/wxa-runner-extractor");
486
645
 
@@ -758,7 +917,7 @@ ${pageWindow(build2, pagePath).disableScroll ? "<style>body{overflow-y:hidden}</
758
917
  }
759
918
  function workerScript(build2, baseLib) {
760
919
  const modules = build2.workerModules.map((id) => `/__wxa__/module/${encodeURI(id)}?v=${build2.version}`);
761
- const prelude = `var __wxConfig = ${toJs(build2.wxConfig)};
920
+ const prelude = `var __wxConfig = ${toJs(build2.wxConfig)}; __wxConfig.env.USER_DATA_PATH = location.origin + "/__wxa__/usr/" + new URL(location.href).searchParams.get("session");
762
921
  importScripts('/__wxa__/runtime/worker.js');
763
922
  `;
764
923
  if (!baseLib.hasWorker) {
@@ -840,13 +999,62 @@ function vlq(value) {
840
999
  return out;
841
1000
  }
842
1001
 
1002
+ // src/server/forwardConsole.ts
1003
+ var MAX_LENGTH = 4e3;
1004
+ var LIMIT_PER_SECOND = 20;
1005
+ var CONTROL_CHARS = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
1006
+ function printBrowserReports(socket, logger) {
1007
+ let windowStart = 0;
1008
+ let printed = 0;
1009
+ let dropped = 0;
1010
+ socket.on("message", (data) => {
1011
+ const report = parseReport(data);
1012
+ if (!report) return;
1013
+ const now = Date.now();
1014
+ if (now - windowStart >= 1e3) {
1015
+ windowStart = now;
1016
+ printed = 0;
1017
+ }
1018
+ if (printed >= LIMIT_PER_SECOND) {
1019
+ if (dropped++ === 0) {
1020
+ setTimeout(() => {
1021
+ logger.warn(`[wxa-runner] \u6D4F\u89C8\u5668 1 \u79D2\u5185\u8F6C\u53D1\u7684\u62A5\u9519\u592A\u591A\uFF0C\u7701\u7565\u4E86 ${dropped} \u6761`);
1022
+ dropped = 0;
1023
+ }, windowStart + 1e3 - now).unref();
1024
+ }
1025
+ return;
1026
+ }
1027
+ printed++;
1028
+ logger[report.level](`[wxa-runner] ${clean(report.text)}`);
1029
+ });
1030
+ }
1031
+ function parseReport(data) {
1032
+ try {
1033
+ const report = JSON.parse(data.toString());
1034
+ if (report?.type === "console" && (report.level === "warn" || report.level === "error") && typeof report.text === "string") return report;
1035
+ } catch {
1036
+ }
1037
+ return null;
1038
+ }
1039
+ function clean(text) {
1040
+ const printable = text.replace(CONTROL_CHARS, "");
1041
+ return printable.length > MAX_LENGTH ? `${printable.slice(0, MAX_LENGTH)}\u2026\uFF08\u5DF2\u622A\u65AD\uFF09` : printable;
1042
+ }
1043
+ function ignoreConsoleHook(sourceMap) {
1044
+ const map = JSON.parse(sourceMap);
1045
+ map.x_google_ignoreList = map.sources.flatMap((source, index) => source.endsWith("consoleHook.ts") ? [index] : []);
1046
+ return JSON.stringify(map);
1047
+ }
1048
+
843
1049
  // src/server/proxy.ts
1050
+ var import_node_stream = require("stream");
1051
+ var import_promises = require("stream/promises");
844
1052
  var DROPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding", "keep-alive", "upgrade", "expect"]);
845
1053
  var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection", "set-cookie"]);
846
1054
  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}"`);
1055
+ const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1056
+ if (!target) {
1057
+ res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${req.originalUrl}"`);
850
1058
  return;
851
1059
  }
852
1060
  let requestHeaders = {};
@@ -868,31 +1076,47 @@ async function handleProxy(req, res) {
868
1076
  headers["content-type"] = browserContentType;
869
1077
  }
870
1078
  const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
871
- let upstream;
1079
+ const controller = new AbortController();
1080
+ const cancel = () => controller.abort();
1081
+ res.once("close", cancel);
872
1082
  try {
873
- upstream = await fetch(target, {
874
- method: req.method,
875
- headers,
876
- body: hasBody ? req.body : void 0,
877
- redirect: "follow"
1083
+ let upstream;
1084
+ try {
1085
+ upstream = await fetch(target, {
1086
+ method: req.method,
1087
+ headers,
1088
+ body: hasBody ? req.body : void 0,
1089
+ redirect: "follow",
1090
+ signal: controller.signal
1091
+ });
1092
+ } catch (error) {
1093
+ if (res.destroyed || controller.signal.aborted) return;
1094
+ const cause = error?.cause?.message;
1095
+ res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
1096
+ return;
1097
+ }
1098
+ const responseHeaders = {};
1099
+ upstream.headers.forEach((value, key) => {
1100
+ responseHeaders[key] = value;
1101
+ });
1102
+ const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
1103
+ res.status(upstream.status);
1104
+ upstream.headers.forEach((value, key) => {
1105
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) res.setHeader(key, value);
878
1106
  });
1107
+ res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
1108
+ res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1109
+ res.flushHeaders();
1110
+ if (!upstream.body) {
1111
+ res.end();
1112
+ return;
1113
+ }
1114
+ await (0, import_promises.pipeline)(import_node_stream.Readable.fromWeb(upstream.body), res);
879
1115
  } catch (error) {
880
- const cause = error?.cause?.message;
881
- res.status(502).set("x-wxa-proxy-error", "1").send(cause || error.message);
882
- return;
1116
+ if (!res.destroyed) res.destroy(error);
1117
+ } finally {
1118
+ res.off("close", cancel);
883
1119
  }
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
1120
  }
897
1121
 
898
1122
  // src/server/hmr.ts
@@ -910,8 +1134,8 @@ function planHotUpdate(files, units) {
910
1134
 
911
1135
  // src/server/developerTicket.ts
912
1136
  var import_child_process = require("child_process");
913
- var import_crypto = require("crypto");
914
- var import_promises = require("timers/promises");
1137
+ var import_crypto2 = require("crypto");
1138
+ var import_promises2 = require("timers/promises");
915
1139
  var import_ws = require("ws");
916
1140
  var import_wxa_runner_extractor3 = require("@open-wxa/wxa-runner-extractor");
917
1141
  var AUTO_PORT = 9420;
@@ -960,7 +1184,7 @@ async function requestTicketOnceWindowReady(port, projectPath) {
960
1184
  } catch (error) {
961
1185
  if (!(error instanceof WindowNotFound)) throw error;
962
1186
  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);
1187
+ await (0, import_promises2.setTimeout)(WINDOW_RETRY_INTERVAL);
964
1188
  }
965
1189
  }
966
1190
  }
@@ -1002,7 +1226,7 @@ function cliErrorMessage(stdout) {
1002
1226
  }
1003
1227
  function requestTicket(port) {
1004
1228
  return new Promise((resolve, reject) => {
1005
- const id = `wxa-runner-${(0, import_crypto.randomUUID)()}`;
1229
+ const id = `wxa-runner-${(0, import_crypto2.randomUUID)()}`;
1006
1230
  const socket = new import_ws.WebSocket(`ws://127.0.0.1:${port}`);
1007
1231
  const settle = (finish) => {
1008
1232
  clearTimeout(timer);
@@ -1153,7 +1377,7 @@ function builtinSetupModule() {
1153
1377
  }
1154
1378
 
1155
1379
  // src/server/watcher.ts
1156
- var import_crypto2 = require("crypto");
1380
+ var import_crypto3 = require("crypto");
1157
1381
  var import_fs3 = __toESM(require("fs"));
1158
1382
  var import_path3 = __toESM(require("path"));
1159
1383
  var import_chokidar = __toESM(require("chokidar"));
@@ -1197,7 +1421,7 @@ function updateFingerprint(fingerprints, file, event) {
1197
1421
  if (event !== "add" && event !== "change" || !FINGERPRINTED.test(file)) return false;
1198
1422
  let next;
1199
1423
  try {
1200
- next = (0, import_crypto2.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1424
+ next = (0, import_crypto3.createHash)("sha1").update(import_fs3.default.readFileSync(file)).digest("hex");
1201
1425
  } catch {
1202
1426
  return false;
1203
1427
  }
@@ -1224,8 +1448,9 @@ var DevServer = class {
1224
1448
  this.sockets = null;
1225
1449
  this.watcher = null;
1226
1450
  this.address = "";
1451
+ this.lanAccess = false;
1227
1452
  /** 运行时生成的临时文件(canvas 导出、拍照等),随服务器进程存在 */
1228
- this.tempFiles = /* @__PURE__ */ new Map();
1453
+ this.fileSessions = /* @__PURE__ */ new Map();
1229
1454
  this.logger = options.logger || console;
1230
1455
  this.project = new MiniProgram(import_path4.default.resolve(options.projectPath));
1231
1456
  this.baseLib = (0, import_wxa_runner_extractor5.resolveBaseLib)({ version: options.baseLibVersion, cacheDir: options.baseLibCacheDir });
@@ -1244,9 +1469,16 @@ var DevServer = class {
1244
1469
  this.setupEnv = collectSetupEnv(process.env);
1245
1470
  this.app = this.createApp();
1246
1471
  }
1472
+ /** 本机访问的地址 */
1247
1473
  get url() {
1248
1474
  return this.address;
1249
1475
  }
1476
+ /** 局域网里其他设备访问的地址,每块网卡一个;只监听本机时为空 */
1477
+ get networkUrls() {
1478
+ if (!this.lanAccess) return [];
1479
+ const { port } = new URL(this.address);
1480
+ return Object.values(import_os.default.networkInterfaces()).flat().filter((net2) => net2 && net2.family === "IPv4" && !net2.internal).map((net2) => `http://${net2.address}:${port}`);
1481
+ }
1250
1482
  async start() {
1251
1483
  try {
1252
1484
  const build2 = await this.builder.build();
@@ -1256,7 +1488,11 @@ var DevServer = class {
1256
1488
  ${error.message}`);
1257
1489
  }
1258
1490
  const server = import_http.default.createServer(this.app);
1259
- this.sockets = new import_ws2.WebSocketServer({ noServer: true });
1491
+ this.sockets = new import_ws2.WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
1492
+ this.sockets.on("connection", (socket) => {
1493
+ socket.on("error", () => void 0);
1494
+ if (this.options.forwardConsole) printBrowserReports(socket, this.logger);
1495
+ });
1260
1496
  server.on("upgrade", (req, socket, head) => {
1261
1497
  if (req.url?.startsWith("/__wxa__/ws")) {
1262
1498
  this.sockets.handleUpgrade(req, socket, head, (ws) => this.sockets.emit("connection", ws, req));
@@ -1265,13 +1501,10 @@ ${error.message}`);
1265
1501
  }
1266
1502
  });
1267
1503
  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
- });
1504
+ const port = await listen(server, this.options.port, host, this.logger);
1272
1505
  this.server = server;
1273
- const { port } = server.address();
1274
- this.address = `http://${host}:${port}`;
1506
+ this.lanAccess = LOCAL_HOST_OF_ANY_ADDRESS.has(host);
1507
+ this.address = `http://${LOCAL_HOST_OF_ANY_ADDRESS.get(host) ?? host}:${port}`;
1275
1508
  if (this.options.watch !== false) this.startWatching();
1276
1509
  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
1510
  return this.address;
@@ -1286,6 +1519,7 @@ ${error.message}`);
1286
1519
  server.closeAllConnections();
1287
1520
  await new Promise((resolve) => server.close(() => resolve()));
1288
1521
  }
1522
+ this.fileSessions.clear();
1289
1523
  }
1290
1524
  createApp() {
1291
1525
  const app = (0, import_express2.default)();
@@ -1300,7 +1534,13 @@ ${error.message}`);
1300
1534
  });
1301
1535
  app.get("/__wxa__/app-info.json", async (_req, res) => {
1302
1536
  noStore(res);
1303
- const base = { appid: this.project.appid, baseLibVersion: this.baseLib.version, device: this.device, devtoolsLogin: this.devtoolsLogin };
1537
+ const base = {
1538
+ appid: this.project.appid,
1539
+ baseLibVersion: this.baseLib.version,
1540
+ device: this.device,
1541
+ devtoolsLogin: this.devtoolsLogin,
1542
+ forwardConsole: this.options.forwardConsole ?? false
1543
+ };
1304
1544
  try {
1305
1545
  const build2 = await this.plugins.refresh(await this.builder.build());
1306
1546
  const info = { ...base, version: build2.version, wxConfig: build2.wxConfig, pages: build2.pages };
@@ -1379,7 +1619,18 @@ ${error.message}`);
1379
1619
  app.get("/__wxa__/page-frame.js", this.withBuild((build2, _req, res) => {
1380
1620
  res.type("js").send(build2.pageFrame);
1381
1621
  }));
1382
- app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), { cacheControl: false }));
1622
+ app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
1623
+ const file = import_path4.default.join(this.runtimeDir(), "frame.js.map");
1624
+ if (!import_fs4.default.existsSync(file)) return next();
1625
+ res.type("json").send(ignoreConsoleHook(import_fs4.default.readFileSync(file, "utf8")));
1626
+ });
1627
+ app.use("/__wxa__/runtime", import_express2.default.static(this.runtimeDir(), {
1628
+ cacheControl: false,
1629
+ // Service Worker 默认只能接管自己所在目录下的页面,宿主页面在根路径
1630
+ setHeaders: (res, file) => {
1631
+ if (import_path4.default.basename(file) === "sw.js") res.setHeader("Service-Worker-Allowed", "/");
1632
+ }
1633
+ }));
1383
1634
  const sourceMaps = /* @__PURE__ */ new Map();
1384
1635
  app.get(/^\/__baselib__\/(.+\.js)\.map$/, (req, res, next) => {
1385
1636
  const file = import_path4.default.resolve(this.baseLib.dir, req.params[0]);
@@ -1393,16 +1644,45 @@ ${error.message}`);
1393
1644
  if (file.endsWith(".js")) res.setHeader("SourceMap", `${import_path4.default.basename(file)}.map`);
1394
1645
  }
1395
1646
  }));
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) => {
1647
+ app.use(PROXY_PREFIX, import_express2.default.raw({ type: () => true, limit: "100mb" }), handleProxy);
1648
+ app.use(MOCK_PREFIX, (_req, res) => {
1649
+ 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");
1650
+ });
1651
+ const sessionFiles = (session) => {
1652
+ if (!/^[a-z0-9-]{1,80}$/i.test(session)) return null;
1653
+ let files = this.fileSessions.get(session);
1654
+ if (!files) {
1655
+ files = new FileStore([`/__wxa__/usr/${session}`, `/__wxa__/tmp/${session}`]);
1656
+ this.fileSessions.set(session, files);
1657
+ }
1658
+ return files;
1659
+ };
1660
+ app.post("/__wxa__/fs/:session", import_express2.default.json({ limit: "150mb" }), (req, res) => {
1661
+ const files = sessionFiles(req.params.session);
1662
+ if (!files) {
1663
+ res.status(400).json({ error: `invalid filesystem session ${req.params.session}` });
1664
+ return;
1665
+ }
1666
+ try {
1667
+ res.json({ result: files.execute(req.body.operation, req.body.args || {}) });
1668
+ } catch (error) {
1669
+ res.json({ error: error.message });
1670
+ }
1671
+ });
1672
+ app.post("/__wxa__/tmp/:session", import_express2.default.raw({ type: () => true, limit: "100mb" }), (req, res) => {
1398
1673
  const type = String(req.query.type || "png");
1399
1674
  if (!/^[a-z0-9]+$/i.test(type)) {
1400
1675
  res.status(400).type("text").send(`\u975E\u6CD5\u7684\u6587\u4EF6\u7C7B\u578B ${type}`);
1401
1676
  return;
1402
1677
  }
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}` });
1678
+ const files = sessionFiles(req.params.session);
1679
+ if (!files) {
1680
+ res.status(400).type("text").send(`\u975E\u6CD5\u7684\u4F1A\u8BDD\u6807\u8BC6 ${req.params.session}`);
1681
+ return;
1682
+ }
1683
+ const filePath = `/__wxa__/tmp/${req.params.session}/${(0, import_crypto4.randomUUID)()}.${type}`;
1684
+ files.write(filePath, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
1685
+ res.json({ url: `${req.protocol}://${req.get("host")}${filePath}` });
1406
1686
  });
1407
1687
  app.get("/__wxa__/region/:kind", (req, res) => {
1408
1688
  const kind = req.params.kind;
@@ -1413,14 +1693,16 @@ ${error.message}`);
1413
1693
  }
1414
1694
  res.type("text/plain; charset=utf-8").set("Cache-Control", "public, max-age=86400").send(data);
1415
1695
  });
1416
- app.get("/__wxa__/tmp/:name", (req, res) => {
1417
- const data = this.tempFiles.get(req.params.name);
1418
- if (!data) {
1696
+ app.get("/__wxa__/:kind(usr|tmp)/:session/*", (req, res) => {
1697
+ const files = this.fileSessions.get(req.params.session);
1698
+ const node = files?.stat(`/__wxa__/${req.params.kind}/${req.params.session}/` + req.params[0]);
1699
+ if (!node || node.dir) {
1419
1700
  res.status(404).end();
1420
1701
  return;
1421
1702
  }
1422
- res.type(import_path4.default.extname(req.params.name));
1423
- res.setHeader("ETag", `"${req.params.name}"`);
1703
+ const data = node.data;
1704
+ res.type(import_path4.default.extname(req.params[0]));
1705
+ res.setHeader("ETag", `"${node.etag}"`);
1424
1706
  res.setHeader("Accept-Ranges", "bytes");
1425
1707
  const ranges = req.range(data.length);
1426
1708
  if (ranges === -1) {
@@ -1436,8 +1718,15 @@ ${error.message}`);
1436
1718
  }
1437
1719
  res.send(data);
1438
1720
  });
1439
- app.delete("/__wxa__/tmp/:name", (req, res) => {
1440
- res.status(this.tempFiles.delete(req.params.name) ? 204 : 404).end();
1721
+ app.delete("/__wxa__/tmp/:session/*", (req, res) => {
1722
+ try {
1723
+ const files = this.fileSessions.get(req.params.session);
1724
+ if (!files) throw new Error("file not exist");
1725
+ files.remove(`/__wxa__/tmp/${req.params.session}/` + req.params[0], { dir: false });
1726
+ res.status(204).end();
1727
+ } catch {
1728
+ res.status(404).end();
1729
+ }
1441
1730
  });
1442
1731
  if (this.ticketSource) app.use("/__wxa__/api", devtoolsLoginRouter(this.project.appid, this.ticketSource));
1443
1732
  app.use("/__wxa__/webview", import_express2.default.static(root));
@@ -1526,6 +1815,38 @@ ${message}`);
1526
1815
  });
1527
1816
  }
1528
1817
  };
1818
+ var DEFAULT_PORT = 3e3;
1819
+ var LOCAL_HOST_OF_ANY_ADDRESS = /* @__PURE__ */ new Map([["0.0.0.0", "127.0.0.1"], ["::", "localhost"]]);
1820
+ async function listen(server, port, host, logger) {
1821
+ for (let candidate = port ?? DEFAULT_PORT; ; candidate++) {
1822
+ const taken = LOCAL_HOST_OF_ANY_ADDRESS.has(host) && await takenOnLoopback(candidate) || !await bind(server, candidate, host);
1823
+ if (!taken) {
1824
+ const actual = server.address().port;
1825
+ if (port === void 0 && actual !== DEFAULT_PORT) logger.warn(`[wxa-runner] \u7AEF\u53E3 ${DEFAULT_PORT} \u5DF2\u88AB\u5360\u7528\uFF0C\u6539\u7528 ${actual}`);
1826
+ return actual;
1827
+ }
1828
+ 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`);
1829
+ }
1830
+ }
1831
+ function bind(server, port, host) {
1832
+ return new Promise((resolve, reject) => {
1833
+ const onError = (error) => error.code === "EADDRINUSE" ? resolve(false) : reject(error);
1834
+ server.once("error", onError);
1835
+ server.listen(port, host, () => {
1836
+ server.off("error", onError);
1837
+ resolve(true);
1838
+ });
1839
+ });
1840
+ }
1841
+ async function takenOnLoopback(port) {
1842
+ for (const address of ["127.0.0.1", "::1"]) {
1843
+ const probe = import_net.default.createServer();
1844
+ const free = await bind(probe, port, address).catch(() => true);
1845
+ if (!free) return true;
1846
+ await new Promise((resolve) => probe.close(resolve));
1847
+ }
1848
+ return false;
1849
+ }
1529
1850
  function resolveSetupFile(option, projectPath) {
1530
1851
  if (option === false) return null;
1531
1852
  if (!option) return findSetupFile(projectPath);