@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.d.mts +27 -10
- package/dist/index.d.ts +27 -10
- package/dist/index.js +380 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +376 -55
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/frame.js +44 -2
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/host.js +443 -175
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/sw.js +56 -0
- package/dist/runtime/sw.js.map +1 -0
- package/dist/{setup-DZ5FeKay.d.mts → setup-Bac8XdhR.d.mts} +5 -1
- package/dist/{setup-DZ5FeKay.d.ts → setup-Bac8XdhR.d.ts} +5 -1
- package/dist/setup.d.mts +1 -1
- package/dist/setup.d.ts +1 -1
- package/dist/test-api.d.mts +2 -2
- package/dist/test-api.d.ts +2 -2
- package/dist/{testApi-BLozOgCt.d.ts → testApi-BOZSXSnV.d.ts} +3 -3
- package/dist/{testApi-CnZOLyWw.d.mts → testApi-DHuEW5Cl.d.mts} +3 -3
- package/package.json +4 -4
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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 =
|
|
812
|
-
if (
|
|
813
|
-
res.status(400).set("x-wxa-proxy-error", "1").send(`invalid url "${
|
|
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
|
-
|
|
1043
|
+
const controller = new AbortController();
|
|
1044
|
+
const cancel = () => controller.abort();
|
|
1045
|
+
res.once("close", cancel);
|
|
836
1046
|
try {
|
|
837
|
-
upstream
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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-${
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
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.
|
|
1361
|
-
app.
|
|
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
|
|
1368
|
-
|
|
1369
|
-
|
|
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__
|
|
1381
|
-
const
|
|
1382
|
-
|
|
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
|
-
|
|
1387
|
-
res.
|
|
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/:
|
|
1404
|
-
|
|
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);
|