@open-wxa/wxa-runner-core 0.10.0 → 0.11.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.d.mts +46 -6
- package/dist/index.d.ts +46 -6
- package/dist/index.js +487 -297
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +476 -292
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/host.js +23 -10
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/sw.js +24 -20
- package/dist/runtime/sw.js.map +1 -1
- package/dist/{setup-Bac8XdhR.d.mts → setup-DhDDVlit.d.mts} +4 -0
- package/dist/{setup-Bac8XdhR.d.ts → setup-DhDDVlit.d.ts} +4 -0
- 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-DHuEW5Cl.d.mts → testApi-BbK_1vhj.d.mts} +1 -1
- package/dist/{testApi-BOZSXSnV.d.ts → testApi-DbTnccYV.d.ts} +1 -1
- package/dist/web.d.mts +268 -0
- package/dist/web.d.ts +268 -0
- package/dist/web.js +700 -0
- package/dist/web.js.map +1 -0
- package/dist/web.mjs +677 -0
- package/dist/web.mjs.map +1 -0
- package/package.json +11 -3
package/dist/index.mjs
CHANGED
|
@@ -3,20 +3,94 @@ import {
|
|
|
3
3
|
} from "./chunk-NGXYLLYS.mjs";
|
|
4
4
|
|
|
5
5
|
// src/server/DevServer.ts
|
|
6
|
-
import { randomUUID as
|
|
6
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
7
7
|
|
|
8
8
|
// src/server/FileStore.ts
|
|
9
|
-
|
|
9
|
+
var storeSequence = 0;
|
|
10
|
+
var textEncoder = new TextEncoder();
|
|
11
|
+
var textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
12
|
+
function normalizeEncoding(encoding) {
|
|
13
|
+
switch (String(encoding).toLowerCase()) {
|
|
14
|
+
case "utf8":
|
|
15
|
+
case "utf-8":
|
|
16
|
+
return "utf8";
|
|
17
|
+
case "utf16le":
|
|
18
|
+
case "utf-16le":
|
|
19
|
+
case "ucs2":
|
|
20
|
+
case "ucs-2":
|
|
21
|
+
return "utf16le";
|
|
22
|
+
case "latin1":
|
|
23
|
+
case "binary":
|
|
24
|
+
return "latin1";
|
|
25
|
+
case "ascii":
|
|
26
|
+
return "ascii";
|
|
27
|
+
case "base64":
|
|
28
|
+
case "base64url":
|
|
29
|
+
return String(encoding).toLowerCase();
|
|
30
|
+
case "hex":
|
|
31
|
+
return "hex";
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unknown encoding: ${encoding}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function encodeText(text, encoding) {
|
|
37
|
+
const format = normalizeEncoding(encoding);
|
|
38
|
+
if (format === "utf8") return textEncoder.encode(text);
|
|
39
|
+
if (format === "base64" || format === "base64url") {
|
|
40
|
+
let base64 = text.split("=", 1)[0].replace(/-/g, "+").replace(/_/g, "/").replace(/[^A-Za-z0-9+/]/g, "");
|
|
41
|
+
if (base64.length % 4 === 1) base64 = base64.slice(0, -1);
|
|
42
|
+
base64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
43
|
+
return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
|
44
|
+
}
|
|
45
|
+
if (format === "hex") {
|
|
46
|
+
const bytes2 = [];
|
|
47
|
+
for (let index = 0; index + 1 < text.length; index += 2) {
|
|
48
|
+
const pair = text.slice(index, index + 2);
|
|
49
|
+
if (!/^[\da-f]{2}$/i.test(pair)) break;
|
|
50
|
+
bytes2.push(parseInt(pair, 16));
|
|
51
|
+
}
|
|
52
|
+
return new Uint8Array(bytes2);
|
|
53
|
+
}
|
|
54
|
+
const bytes = new Uint8Array(text.length * (format === "utf16le" ? 2 : 1));
|
|
55
|
+
for (let index = 0; index < text.length; index++) {
|
|
56
|
+
const code = text.charCodeAt(index);
|
|
57
|
+
if (format === "utf16le") {
|
|
58
|
+
bytes[index * 2] = code & 255;
|
|
59
|
+
bytes[index * 2 + 1] = code >>> 8;
|
|
60
|
+
} else bytes[index] = code & 255;
|
|
61
|
+
}
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
function decodeText(bytes, encoding) {
|
|
65
|
+
const format = normalizeEncoding(encoding);
|
|
66
|
+
if (format === "utf8") return textDecoder.decode(bytes);
|
|
67
|
+
if (format === "hex") return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for (let offset = 0; offset < bytes.length; offset += 32768) {
|
|
70
|
+
const chunk = bytes.subarray(offset, offset + 32768);
|
|
71
|
+
const codes = [];
|
|
72
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
73
|
+
if (format === "utf16le") {
|
|
74
|
+
if (index + 1 < chunk.length) codes.push(chunk[index] | chunk[++index] << 8);
|
|
75
|
+
} else codes.push(format === "ascii" ? chunk[index] & 127 : chunk[index]);
|
|
76
|
+
}
|
|
77
|
+
chunks.push(String.fromCharCode(...codes));
|
|
78
|
+
}
|
|
79
|
+
const text = chunks.join("");
|
|
80
|
+
if (format === "base64") return btoa(text);
|
|
81
|
+
if (format === "base64url") return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
82
|
+
return text;
|
|
83
|
+
}
|
|
10
84
|
var FileStore = class {
|
|
11
85
|
/** @param roots 预先创建的目录(如用户目录)。rmdir 不能删除它们和它们的上级目录:删掉之后再写 USER_DATA_PATH 都会失败 */
|
|
12
86
|
constructor(roots = []) {
|
|
13
|
-
this.etagPrefix = randomUUID()
|
|
87
|
+
this.etagPrefix = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${++storeSequence}-${Math.random().toString(36).slice(2)}`;
|
|
14
88
|
this.version = 0;
|
|
15
|
-
this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data:
|
|
89
|
+
this.nodes = /* @__PURE__ */ new Map([["/", { dir: true, data: new Uint8Array(0), mtime: Date.now(), etag: "" }]]);
|
|
16
90
|
this.operations = {
|
|
17
91
|
readFile: ({ filePath, encoding }) => {
|
|
18
92
|
const data = this.read(filePath);
|
|
19
|
-
return encoding === void 0 ? { data: data
|
|
93
|
+
return encoding === void 0 ? { data: decodeText(data, "base64"), binary: true } : { data: decodeText(data, encoding) };
|
|
20
94
|
},
|
|
21
95
|
writeFile: (args) => this.writeEncoded(args, false),
|
|
22
96
|
appendFile: (args) => this.writeEncoded(args, true),
|
|
@@ -78,16 +152,21 @@ var FileStore = class {
|
|
|
78
152
|
if (!recursive) throw new Error(`no such file or directory ${dirPath}`);
|
|
79
153
|
this.mkdir(parent, true);
|
|
80
154
|
}
|
|
81
|
-
this.nodes.set(target, { dir: true, data:
|
|
155
|
+
this.nodes.set(target, { dir: true, data: new Uint8Array(0), mtime: Date.now(), etag: "" });
|
|
82
156
|
}
|
|
83
157
|
write(filePath, input, append = false) {
|
|
84
|
-
const data =
|
|
158
|
+
const data = typeof input === "string" ? textEncoder.encode(input) : new Uint8Array(input);
|
|
85
159
|
const target = this.normalize(filePath);
|
|
86
160
|
const parent = target.slice(0, target.lastIndexOf("/")) || "/";
|
|
87
161
|
if (!this.nodes.get(parent)?.dir) throw new Error(`no such file or directory ${filePath}`);
|
|
88
162
|
const existing = this.nodes.get(target);
|
|
89
163
|
if (existing?.dir) throw new Error(`illegal operation on a directory ${filePath}`);
|
|
90
|
-
|
|
164
|
+
let contents = data;
|
|
165
|
+
if (append && existing) {
|
|
166
|
+
contents = new Uint8Array(existing.data.length + data.length);
|
|
167
|
+
contents.set(existing.data);
|
|
168
|
+
contents.set(data, existing.data.length);
|
|
169
|
+
}
|
|
91
170
|
this.nodes.set(target, { dir: false, data: contents, mtime: Date.now(), etag: `${this.etagPrefix}-${++this.version}` });
|
|
92
171
|
}
|
|
93
172
|
read(filePath) {
|
|
@@ -122,7 +201,7 @@ var FileStore = class {
|
|
|
122
201
|
return this.operations[operation](args);
|
|
123
202
|
}
|
|
124
203
|
writeEncoded(args, append) {
|
|
125
|
-
this.write(args.filePath,
|
|
204
|
+
this.write(args.filePath, encodeText(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8"), append);
|
|
126
205
|
return {};
|
|
127
206
|
}
|
|
128
207
|
existing(filePath) {
|
|
@@ -311,6 +390,11 @@ function describeError(project, error) {
|
|
|
311
390
|
return error?.message ?? String(error);
|
|
312
391
|
}
|
|
313
392
|
|
|
393
|
+
// src/project/toJs.ts
|
|
394
|
+
function toJs(value) {
|
|
395
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
396
|
+
}
|
|
397
|
+
|
|
314
398
|
// src/project/wxConfig.ts
|
|
315
399
|
var DEFAULT_WINDOW = {
|
|
316
400
|
navigationBarBackgroundColor: "#000000",
|
|
@@ -650,9 +734,6 @@ try { require(${toJs(file)}); } catch (error) { console.error(${toJs("[wxa-runne
|
|
|
650
734
|
function rewriteSafeAreaInsets(code) {
|
|
651
735
|
return code.replace(/\b(?:env|constant)\(\s*safe-area-inset-(top|right|bottom|left)\b/g, "var(--wxa-safe-area-inset-$1");
|
|
652
736
|
}
|
|
653
|
-
function toJs(value) {
|
|
654
|
-
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
655
|
-
}
|
|
656
737
|
function classifyFile(file) {
|
|
657
738
|
if (file.endsWith(".json")) return "config";
|
|
658
739
|
if (file.endsWith(".wxml") || file.endsWith(".wxs")) return "wxml";
|
|
@@ -846,155 +927,7 @@ function fromServerPath(prefix, serverPath) {
|
|
|
846
927
|
}
|
|
847
928
|
}
|
|
848
929
|
|
|
849
|
-
// src/server/
|
|
850
|
-
import { readWxvpkgFile, readWxvpkgIndex } from "@open-wxa/wxa-runner-extractor";
|
|
851
|
-
|
|
852
|
-
// src/server/wechatDev.ts
|
|
853
|
-
import { TICKET_ENV } from "@open-wxa/wxa-runner-extractor";
|
|
854
|
-
var BASE_URL = "https://servicewechat.com";
|
|
855
|
-
var CLIENT_VERSION = "1021810240";
|
|
856
|
-
var DEFAULT_TIMEOUT = 1e4;
|
|
857
|
-
var ERROR_HINTS = {
|
|
858
|
-
// 40001:开发者工具刷新过 ticket,旧的随即作废
|
|
859
|
-
40001: "\uFF08\u767B\u5F55\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u91CD\u65B0\u767B\u5F55\uFF09",
|
|
860
|
-
40029: "\uFF08\u767B\u5F55\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u91CD\u65B0\u767B\u5F55\uFF09",
|
|
861
|
-
85023: "\uFF08\u5F53\u524D\u5FAE\u4FE1\u8D26\u53F7\u4E0D\u662F\u8BE5\u5C0F\u7A0B\u5E8F\u7684\u5F00\u53D1\u8005\uFF09"
|
|
862
|
-
};
|
|
863
|
-
function requireUsableTicket(ticket) {
|
|
864
|
-
if (!ticket) throw new Error(`\u672A\u627E\u5230\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u7684\u767B\u5F55\u6001\uFF0C\u8BF7\u767B\u5F55\u5F00\u53D1\u8005\u5DE5\u5177\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${TICKET_ENV}`);
|
|
865
|
-
if (ticket.expiredAt !== void 0 && ticket.expiredAt <= Date.now()) {
|
|
866
|
-
throw new Error("\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u7684\u767B\u5F55\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u6253\u5F00\u5F00\u53D1\u8005\u5DE5\u5177\u5237\u65B0\u767B\u5F55\u6001");
|
|
867
|
-
}
|
|
868
|
-
return ticket.newticket;
|
|
869
|
-
}
|
|
870
|
-
function explainErrcode(errcode) {
|
|
871
|
-
return errcode !== void 0 && ERROR_HINTS[errcode] || "";
|
|
872
|
-
}
|
|
873
|
-
function requireOk(body, action) {
|
|
874
|
-
const errcode = body.baseresponse?.errcode;
|
|
875
|
-
if (errcode !== 0) throw new Error(`${action} ${errcode}: ${body.baseresponse?.errmsg || "\u672A\u77E5\u9519\u8BEF"}${explainErrcode(errcode)}`);
|
|
876
|
-
return body;
|
|
877
|
-
}
|
|
878
|
-
var WechatDevClient = class {
|
|
879
|
-
/**
|
|
880
|
-
* @param findTicket 每次请求都重新取:开发者工具重新登录、切换账号后不用重启服务器
|
|
881
|
-
*/
|
|
882
|
-
constructor(appid, findTicket) {
|
|
883
|
-
this.appid = appid;
|
|
884
|
-
this.findTicket = findTicket;
|
|
885
|
-
}
|
|
886
|
-
/**
|
|
887
|
-
* POST JSON 到 path(如 wxa-dev-logic/jslogin),带上 appid 和开发者 ticket
|
|
888
|
-
*
|
|
889
|
-
* @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
|
|
890
|
-
*/
|
|
891
|
-
async post(path7, body, timeout = DEFAULT_TIMEOUT) {
|
|
892
|
-
const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
|
|
893
|
-
const response = await fetch(`${BASE_URL}/${path7}?${query}`, {
|
|
894
|
-
method: "POST",
|
|
895
|
-
headers: { "Content-Type": "application/json" },
|
|
896
|
-
body: JSON.stringify(body),
|
|
897
|
-
signal: AbortSignal.timeout(timeout)
|
|
898
|
-
}).catch((error) => {
|
|
899
|
-
throw new Error(error.name === "TimeoutError" ? `\u8BF7\u6C42\u5FAE\u4FE1\u670D\u52A1\u5668\u8D85\u65F6\uFF08${timeout / 1e3}s\uFF09` : `\u8BF7\u6C42\u5FAE\u4FE1\u670D\u52A1\u5668\u5931\u8D25\uFF1A${error.cause?.message ?? error.message}`);
|
|
900
|
-
});
|
|
901
|
-
if (!response.ok) throw new Error(`\u5FAE\u4FE1\u670D\u52A1\u5668\u8FD4\u56DE HTTP ${response.status}`);
|
|
902
|
-
return response;
|
|
903
|
-
}
|
|
904
|
-
/** 同 post,返回解析后的 JSON */
|
|
905
|
-
async postJson(path7, body) {
|
|
906
|
-
return (await this.post(path7, body)).json();
|
|
907
|
-
}
|
|
908
|
-
};
|
|
909
|
-
|
|
910
|
-
// src/server/plugins.ts
|
|
911
|
-
var DOWNLOAD_TIMEOUT = 3e4;
|
|
912
|
-
var PACKAGE_MAGIC = 190;
|
|
913
|
-
var PluginStore = class {
|
|
914
|
-
/**
|
|
915
|
-
* @param wechat 开启 --devtools-login 时才有,没有时不下载插件
|
|
916
|
-
*/
|
|
917
|
-
constructor(wechat, logger) {
|
|
918
|
-
this.wechat = wechat;
|
|
919
|
-
this.logger = logger;
|
|
920
|
-
/** provider@version → 下载结果;失败的不留,下次打开小程序时重试 */
|
|
921
|
-
this.downloads = /* @__PURE__ */ new Map();
|
|
922
|
-
/**
|
|
923
|
-
* 这次打开小程序时决定用哪些插件。同一次打开的逻辑层、业务模块、渲染层等请求都按它处理:
|
|
924
|
-
* 各自去下载的话,下载失败时每个请求都会重试,结果还可能前后不一,页面列表和逻辑层对不上
|
|
925
|
-
*/
|
|
926
|
-
this.selection = null;
|
|
927
|
-
/** 没开 --devtools-login 的提示每个插件只说一次 */
|
|
928
|
-
this.warned = /* @__PURE__ */ new Set();
|
|
929
|
-
}
|
|
930
|
-
/** 打开小程序时(宿主取 app-info)调用:重新决定用哪些插件,下载过的直接用,上次失败的重试 */
|
|
931
|
-
async refresh(build2) {
|
|
932
|
-
this.selection = this.select(build2);
|
|
933
|
-
return withPlugins(build2, await this.selection);
|
|
934
|
-
}
|
|
935
|
-
/** 其余请求沿用这次打开时的决定,不触发下载;还没决定过时(如直接请求逻辑层)先决定一次 */
|
|
936
|
-
async current(build2) {
|
|
937
|
-
this.selection ??= this.select(build2);
|
|
938
|
-
return withPlugins(build2, await this.selection);
|
|
939
|
-
}
|
|
940
|
-
/** app.json 声明的插件;下载不了的跳过,在终端说明原因,小程序自己的页面照常可用 */
|
|
941
|
-
async select(build2) {
|
|
942
|
-
const declared = build2.wxConfig.plugins || {};
|
|
943
|
-
const results = await Promise.all(Object.entries(declared).map(
|
|
944
|
-
([alias, { provider, version }]) => provider && version ? this.load(alias, provider, version) : null
|
|
945
|
-
));
|
|
946
|
-
return results.filter((plugin) => !!plugin);
|
|
947
|
-
}
|
|
948
|
-
async load(alias, provider, version) {
|
|
949
|
-
const label = `\u63D2\u4EF6 ${alias}\uFF08${provider}@${version}\uFF09`;
|
|
950
|
-
if (!this.wechat) {
|
|
951
|
-
this.warnOnce(label, `[wxa-runner] ${label} \u7684\u4EE3\u7801\u8981\u7528\u5F00\u53D1\u8005\u7684\u767B\u5F55\u6001\u5411\u5FAE\u4FE1\u4E0B\u8F7D\uFF0C\u542F\u52A8\u65F6\u52A0 --devtools-login`);
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
const key = `${provider}@${version}`;
|
|
955
|
-
let download = this.downloads.get(key);
|
|
956
|
-
if (!download) {
|
|
957
|
-
download = this.download(this.wechat, alias, provider, version);
|
|
958
|
-
this.downloads.set(key, download);
|
|
959
|
-
download.catch(() => this.downloads.delete(key));
|
|
960
|
-
}
|
|
961
|
-
try {
|
|
962
|
-
return await download;
|
|
963
|
-
} catch (error) {
|
|
964
|
-
this.logger.warn(`[wxa-runner] ${label} \u4E0B\u8F7D\u5931\u8D25\uFF1A${error.message}`);
|
|
965
|
-
return null;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
/** @throws 没有登录态、连不上、微信拒绝或返回的不是插件代码包时,消息是原因 */
|
|
969
|
-
async download(wechat, alias, provider, version) {
|
|
970
|
-
const response = await wechat.post("wxa-dev/getplugincode", { plugin_id: provider, plugin_version: version }, DOWNLOAD_TIMEOUT);
|
|
971
|
-
const data = Buffer.from(await response.arrayBuffer());
|
|
972
|
-
if (data[0] !== PACKAGE_MAGIC) {
|
|
973
|
-
let body = {};
|
|
974
|
-
try {
|
|
975
|
-
body = JSON.parse(data.toString("utf8"));
|
|
976
|
-
} catch {
|
|
977
|
-
}
|
|
978
|
-
if (body.baseresponse?.errcode) requireOk(body, "\u5FAE\u4FE1\u62D2\u7EDD\u4E0B\u8F7D");
|
|
979
|
-
throw new Error("\u5FAE\u4FE1\u6CA1\u6709\u8FD4\u56DE\u63D2\u4EF6\u4EE3\u7801");
|
|
980
|
-
}
|
|
981
|
-
const files = new Map(readWxvpkgIndex(data).map((entry) => [entry.name, Buffer.from(readWxvpkgFile(data, entry))]));
|
|
982
|
-
const pluginJson = JSON.parse(files.get("/plugin.json")?.toString("utf8") || "{}");
|
|
983
|
-
return {
|
|
984
|
-
alias,
|
|
985
|
-
provider,
|
|
986
|
-
version,
|
|
987
|
-
files,
|
|
988
|
-
pages: Object.values(pluginJson.pages || {}),
|
|
989
|
-
pageConfigs: pluginJson.page || {}
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
warnOnce(key, message) {
|
|
993
|
-
if (this.warned.has(key)) return;
|
|
994
|
-
this.warned.add(key);
|
|
995
|
-
this.logger.warn(message);
|
|
996
|
-
}
|
|
997
|
-
};
|
|
930
|
+
// src/server/pluginCode.ts
|
|
998
931
|
function withPlugins(build2, plugins) {
|
|
999
932
|
if (!plugins.length) return { ...build2, plugins };
|
|
1000
933
|
return {
|
|
@@ -1139,7 +1072,8 @@ importScripts(${modules.map((url) => toJs(url)).join(", ")});
|
|
|
1139
1072
|
}
|
|
1140
1073
|
function pluginWebviewHtml(build2, plugin, innerPath, pagePath, webviewId, options = {}) {
|
|
1141
1074
|
const id = Number(webviewId) || 0;
|
|
1142
|
-
const
|
|
1075
|
+
const html2 = plugin.files.get(`/${innerPath}.html`);
|
|
1076
|
+
const page = html2 ? new TextDecoder().decode(html2) : `<script>console.error(${JSON.stringify(`[wxa-runner] \u63D2\u4EF6 ${plugin.alias} \u91CC\u6CA1\u6709\u9875\u9762 ${innerPath}`)})</script>`;
|
|
1143
1077
|
return `<!DOCTYPE html>
|
|
1144
1078
|
<html>
|
|
1145
1079
|
<head>
|
|
@@ -1182,6 +1116,94 @@ function escapeHtml(text) {
|
|
|
1182
1116
|
return text.replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
1183
1117
|
}
|
|
1184
1118
|
|
|
1119
|
+
// src/server/appRoutes.ts
|
|
1120
|
+
var DEFAULT_DEVICE = {
|
|
1121
|
+
model: "iPhone X",
|
|
1122
|
+
screenWidth: 375,
|
|
1123
|
+
screenHeight: 812,
|
|
1124
|
+
pixelRatio: 3,
|
|
1125
|
+
statusBarHeight: 44,
|
|
1126
|
+
safeAreaBottom: 34,
|
|
1127
|
+
system: "iOS 16.0",
|
|
1128
|
+
platform: "devtools"
|
|
1129
|
+
};
|
|
1130
|
+
var PLUGIN_SCRIPT = /^\/__wxa__\/plugin\/([^/]+)\/([^/]+)\/(appservice\.js|pageframe\.js)$/;
|
|
1131
|
+
function routeApp(source, url) {
|
|
1132
|
+
const { build: build2 } = source;
|
|
1133
|
+
const pathname = safeDecode(url.pathname);
|
|
1134
|
+
if (pathname === null) return null;
|
|
1135
|
+
const query = url.searchParams;
|
|
1136
|
+
switch (pathname) {
|
|
1137
|
+
case "/__wxa__/appservice.html": {
|
|
1138
|
+
const launchPath = (query.get("path") || "").replace(/^\//, "");
|
|
1139
|
+
const launch = build2.pages.includes(launchPath) ? { path: launchPath, query: Object.fromEntries(new URLSearchParams(query.get("query") || "")) } : { path: build2.pages[0], query: {} };
|
|
1140
|
+
return html(appserviceHtml(build2, launch, { hot: source.hot, isolated: source.isolated }));
|
|
1141
|
+
}
|
|
1142
|
+
case "/__wxa__/subcontext.html": {
|
|
1143
|
+
const id = Number(query.get("id"));
|
|
1144
|
+
if (!Number.isInteger(id) || id <= 0) return { status: 400, type: "text", body: "\u5B50\u4E0A\u4E0B\u6587 id \u65E0\u6548" };
|
|
1145
|
+
return html(subcontextHtml(id, query.get("empty") === "1"));
|
|
1146
|
+
}
|
|
1147
|
+
case "/__wxa__/worker.js":
|
|
1148
|
+
return js(workerScript(build2, source.baseLib));
|
|
1149
|
+
case "/__wxa__/service-bootstrap.js":
|
|
1150
|
+
return js(build2.serviceBootstrap);
|
|
1151
|
+
case "/__wxa__/service-templates.js":
|
|
1152
|
+
return js(build2.serviceTemplates);
|
|
1153
|
+
case "/__wxa__/page-templates.js":
|
|
1154
|
+
return js(build2.pageTemplates);
|
|
1155
|
+
case "/__wxa__/page-styles.js":
|
|
1156
|
+
return js(build2.pageStyles);
|
|
1157
|
+
case "/__wxa__/page-frame.js":
|
|
1158
|
+
return js(build2.pageFrame);
|
|
1159
|
+
}
|
|
1160
|
+
const webview = /^\/__wxa__\/webview\/(.+)\.html$/.exec(pathname);
|
|
1161
|
+
if (webview) {
|
|
1162
|
+
const pagePath = webview[1];
|
|
1163
|
+
if (!build2.pages.includes(pagePath)) return null;
|
|
1164
|
+
const webviewId = Number(query.get("id"));
|
|
1165
|
+
const pluginPage = findPluginPage(build2, pagePath);
|
|
1166
|
+
return html(pluginPage ? pluginWebviewHtml(build2, pluginPage.plugin, pluginPage.innerPath, pagePath, webviewId, { scripts: source.webviewScripts }) : webviewHtml(build2, pagePath, webviewId, { hot: source.hot, scripts: source.webviewScripts }));
|
|
1167
|
+
}
|
|
1168
|
+
const pluginScript = PLUGIN_SCRIPT.exec(pathname);
|
|
1169
|
+
if (pluginScript) {
|
|
1170
|
+
const [, provider, version, file] = pluginScript;
|
|
1171
|
+
const code = build2.plugins.find((plugin) => plugin.provider === provider && plugin.version === version)?.files.get(`/${file}`);
|
|
1172
|
+
return code ? { status: 200, type: "js", body: code } : null;
|
|
1173
|
+
}
|
|
1174
|
+
const pluginFile = /^\/__plugin__\/([^/]+)(\/.+)$/.exec(pathname);
|
|
1175
|
+
if (pluginFile) {
|
|
1176
|
+
const file = build2.plugins.find((plugin) => plugin.provider === pluginFile[1])?.files.get(pluginFile[2]);
|
|
1177
|
+
return file ? { status: 200, type: extname(pluginFile[2]) || "application/octet-stream", body: file } : null;
|
|
1178
|
+
}
|
|
1179
|
+
const module = /^\/__wxa__\/module\/(.+\.js)$/.exec(pathname);
|
|
1180
|
+
if (module) {
|
|
1181
|
+
const id = module[1];
|
|
1182
|
+
if (!build2.modules.includes(id) && !build2.workerModules.includes(id)) {
|
|
1183
|
+
return { status: 404, type: "js", body: `console.error(${JSON.stringify(`[wxa-runner] \u6A21\u5757 ${id} \u4E0D\u5B58\u5728`)})` };
|
|
1184
|
+
}
|
|
1185
|
+
return js(source.moduleScript(id));
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
function extname(filePath) {
|
|
1190
|
+
const match = /\.[^./]+$/.exec(filePath);
|
|
1191
|
+
return match ? match[0] : "";
|
|
1192
|
+
}
|
|
1193
|
+
function html(body) {
|
|
1194
|
+
return { status: 200, type: "html", body };
|
|
1195
|
+
}
|
|
1196
|
+
function js(body) {
|
|
1197
|
+
return { status: 200, type: "js", body };
|
|
1198
|
+
}
|
|
1199
|
+
function safeDecode(pathname) {
|
|
1200
|
+
try {
|
|
1201
|
+
return decodeURIComponent(pathname);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1185
1207
|
// src/server/ignoreList.ts
|
|
1186
1208
|
var STRIDE = 100;
|
|
1187
1209
|
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -1260,48 +1282,64 @@ function ignoreConsoleHook(sourceMap) {
|
|
|
1260
1282
|
// src/server/proxy.ts
|
|
1261
1283
|
import { Readable } from "stream";
|
|
1262
1284
|
import { pipeline } from "stream/promises";
|
|
1285
|
+
|
|
1286
|
+
// src/server/proxyFetch.ts
|
|
1263
1287
|
var DROPPED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["host", "connection", "content-length", "transfer-encoding", "keep-alive", "upgrade", "expect"]);
|
|
1264
1288
|
var DROPPED_RESPONSE_HEADERS = /* @__PURE__ */ new Set(["content-encoding", "content-length", "transfer-encoding", "connection", "set-cookie"]);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1271
|
-
const controller = new AbortController();
|
|
1272
|
-
const cancel = () => controller.abort();
|
|
1273
|
-
res.once("close", cancel);
|
|
1289
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
1290
|
+
async function proxyFetch(request, options = {}) {
|
|
1291
|
+
const url = new URL(request.url);
|
|
1292
|
+
const path7 = url.pathname + url.search;
|
|
1293
|
+
const target = fromServerPath(PROXY_PREFIX, path7);
|
|
1294
|
+
if (!target) return proxyError(400, `invalid url "${path7}"`);
|
|
1274
1295
|
try {
|
|
1296
|
+
let current = new URL(target);
|
|
1297
|
+
if (options.isAllowed && !options.isAllowed(current)) return forbiddenHost(current, 0);
|
|
1298
|
+
const headers = new Headers(
|
|
1299
|
+
forwardedRequestHeaders(request.headers.get("x-wxa-headers") ?? void 0, request.headers.get("content-type") ?? "")
|
|
1300
|
+
);
|
|
1301
|
+
let method = request.method;
|
|
1302
|
+
let body = request.body && !["GET", "HEAD"].includes(method) ? await request.arrayBuffer() : void 0;
|
|
1303
|
+
const send = options.fetch ?? fetch;
|
|
1275
1304
|
let upstream;
|
|
1276
|
-
|
|
1277
|
-
upstream = await
|
|
1278
|
-
method
|
|
1279
|
-
headers
|
|
1280
|
-
body
|
|
1281
|
-
redirect: "follow",
|
|
1282
|
-
signal:
|
|
1305
|
+
for (let hop = 0; ; hop++) {
|
|
1306
|
+
upstream = await send(current.href, {
|
|
1307
|
+
method,
|
|
1308
|
+
headers,
|
|
1309
|
+
body,
|
|
1310
|
+
redirect: options.isAllowed ? "manual" : "follow",
|
|
1311
|
+
signal: request.signal
|
|
1283
1312
|
});
|
|
1284
|
-
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
1287
|
-
|
|
1313
|
+
const location = upstream.headers.get("location");
|
|
1314
|
+
if (!options.isAllowed || !REDIRECT_STATUSES.has(upstream.status) || location === null) break;
|
|
1315
|
+
await upstream.body?.cancel();
|
|
1316
|
+
const next = new URL(location, current);
|
|
1317
|
+
if (hop >= 20) return proxyError(403, `host "${next.hostname}": redirect hop ${hop + 1} exceeds limit 20`);
|
|
1318
|
+
if (!["http:", "https:"].includes(next.protocol) || !options.isAllowed(next)) return forbiddenHost(next, hop + 1);
|
|
1319
|
+
if (upstream.status === 303 && method !== "GET" && method !== "HEAD" || (upstream.status === 301 || upstream.status === 302) && method === "POST") {
|
|
1320
|
+
method = "GET";
|
|
1321
|
+
body = void 0;
|
|
1322
|
+
for (const name of ["content-type", "content-encoding", "content-language", "content-location"]) headers.delete(name);
|
|
1323
|
+
}
|
|
1324
|
+
if (next.origin !== current.origin) {
|
|
1325
|
+
for (const name of ["authorization", "proxy-authorization", "cookie"]) headers.delete(name);
|
|
1326
|
+
}
|
|
1327
|
+
current = next;
|
|
1288
1328
|
}
|
|
1289
|
-
|
|
1329
|
+
return relayResponse(upstream);
|
|
1290
1330
|
} catch (error) {
|
|
1291
|
-
|
|
1292
|
-
} finally {
|
|
1293
|
-
res.off("close", cancel);
|
|
1331
|
+
return proxyError(502, fetchErrorMessage(error));
|
|
1294
1332
|
}
|
|
1295
1333
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
return hasBody ? req.body : void 0;
|
|
1334
|
+
function forbiddenHost(url, hop) {
|
|
1335
|
+
return proxyError(403, `host "${url.hostname}" is not in the allowlist (redirect hop ${hop})`);
|
|
1299
1336
|
}
|
|
1300
|
-
function
|
|
1301
|
-
return
|
|
1337
|
+
function proxyError(status, message) {
|
|
1338
|
+
return new Response(message, { status, headers: { "x-wxa-proxy-error": "1", "content-type": "text/html; charset=utf-8" } });
|
|
1302
1339
|
}
|
|
1303
|
-
function
|
|
1304
|
-
|
|
1340
|
+
function fetchErrorMessage(error) {
|
|
1341
|
+
const detail = error;
|
|
1342
|
+
return detail?.cause?.message || detail?.message || String(error);
|
|
1305
1343
|
}
|
|
1306
1344
|
function forwardedRequestHeaders(encoded, browserContentType = "") {
|
|
1307
1345
|
const multipart = browserContentType.startsWith("multipart/form-data");
|
|
@@ -1316,27 +1354,61 @@ function forwardedRequestHeaders(encoded, browserContentType = "") {
|
|
|
1316
1354
|
}
|
|
1317
1355
|
function parseHeaders(encoded = "{}") {
|
|
1318
1356
|
try {
|
|
1319
|
-
|
|
1357
|
+
const parsed = JSON.parse(encoded);
|
|
1358
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1320
1359
|
} catch {
|
|
1321
1360
|
return {};
|
|
1322
1361
|
}
|
|
1323
1362
|
}
|
|
1324
|
-
|
|
1363
|
+
function relayResponse(upstream) {
|
|
1325
1364
|
const responseHeaders = {};
|
|
1365
|
+
const headers = new Headers();
|
|
1326
1366
|
upstream.headers.forEach((value, key) => {
|
|
1327
1367
|
responseHeaders[key] = value;
|
|
1328
|
-
if (!DROPPED_RESPONSE_HEADERS.has(key))
|
|
1368
|
+
if (!DROPPED_RESPONSE_HEADERS.has(key)) headers.set(key, value);
|
|
1329
1369
|
});
|
|
1330
|
-
|
|
1331
|
-
res.setHeader("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
|
|
1370
|
+
headers.set("x-wxa-headers", encodeURIComponent(JSON.stringify(responseHeaders)));
|
|
1332
1371
|
const setCookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1372
|
+
headers.set("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
|
|
1373
|
+
return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers });
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/server/proxy.ts
|
|
1377
|
+
async function handleProxy(req, res) {
|
|
1378
|
+
const controller = new AbortController();
|
|
1379
|
+
const cancel = () => controller.abort();
|
|
1380
|
+
res.once("close", cancel);
|
|
1381
|
+
try {
|
|
1382
|
+
const headers = new Headers();
|
|
1383
|
+
for (const name of ["x-wxa-headers", "content-type"]) {
|
|
1384
|
+
const value = req.get(name);
|
|
1385
|
+
if (value !== void 0) headers.set(name, value);
|
|
1386
|
+
}
|
|
1387
|
+
const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
|
|
1388
|
+
const request = new globalThis.Request(`http://localhost${req.originalUrl}`, {
|
|
1389
|
+
method: req.method,
|
|
1390
|
+
headers,
|
|
1391
|
+
body: hasBody ? req.body : void 0,
|
|
1392
|
+
signal: controller.signal
|
|
1393
|
+
});
|
|
1394
|
+
const response = await proxyFetch(request);
|
|
1395
|
+
if (res.destroyed || controller.signal.aborted) {
|
|
1396
|
+
await response.body?.cancel();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
response.headers.forEach((value, name) => res.setHeader(name, value));
|
|
1400
|
+
res.status(response.status);
|
|
1401
|
+
res.flushHeaders();
|
|
1402
|
+
if (!response.body) {
|
|
1403
|
+
res.end();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
await pipeline(Readable.fromWeb(response.body), res);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
if (!res.destroyed) res.destroy(error);
|
|
1409
|
+
} finally {
|
|
1410
|
+
res.off("close", cancel);
|
|
1338
1411
|
}
|
|
1339
|
-
await pipeline(Readable.fromWeb(upstream.body), res);
|
|
1340
1412
|
}
|
|
1341
1413
|
|
|
1342
1414
|
// src/server/hmr.ts
|
|
@@ -1355,10 +1427,10 @@ function planHotUpdate(files, units) {
|
|
|
1355
1427
|
|
|
1356
1428
|
// src/server/developerTicket.ts
|
|
1357
1429
|
import { execFile } from "child_process";
|
|
1358
|
-
import { randomUUID
|
|
1430
|
+
import { randomUUID } from "crypto";
|
|
1359
1431
|
import { setTimeout as sleep } from "timers/promises";
|
|
1360
1432
|
import { WebSocket } from "ws";
|
|
1361
|
-
import { findDeveloperTicket, findElectronDevtoolsCli, TICKET_ENV
|
|
1433
|
+
import { findDeveloperTicket, findElectronDevtoolsCli, TICKET_ENV } from "@open-wxa/wxa-runner-extractor";
|
|
1362
1434
|
var AUTO_PORT = 9420;
|
|
1363
1435
|
var CLI_TIMEOUT = 6e4;
|
|
1364
1436
|
var AUTOMATOR_TIMEOUT = 5e3;
|
|
@@ -1382,7 +1454,7 @@ function developerTicketSource(projectPath, logger) {
|
|
|
1382
1454
|
return automatorPort;
|
|
1383
1455
|
};
|
|
1384
1456
|
return async () => {
|
|
1385
|
-
if (process.env[
|
|
1457
|
+
if (process.env[TICKET_ENV]) return { newticket: process.env[TICKET_ENV] };
|
|
1386
1458
|
const port = startOnce();
|
|
1387
1459
|
try {
|
|
1388
1460
|
return await requestTicketOnceWindowReady(await port, projectPath);
|
|
@@ -1447,7 +1519,7 @@ function cliErrorMessage(stdout) {
|
|
|
1447
1519
|
}
|
|
1448
1520
|
function requestTicket(port) {
|
|
1449
1521
|
return new Promise((resolve, reject) => {
|
|
1450
|
-
const id = `wxa-runner-${
|
|
1522
|
+
const id = `wxa-runner-${randomUUID()}`;
|
|
1451
1523
|
const socket = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
1452
1524
|
const settle = (finish) => {
|
|
1453
1525
|
clearTimeout(timer);
|
|
@@ -1487,6 +1559,156 @@ function explainAutomatorError(message) {
|
|
|
1487
1559
|
return new Error(`\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u8FD4\u56DE\u9519\u8BEF\uFF1A${message}`);
|
|
1488
1560
|
}
|
|
1489
1561
|
|
|
1562
|
+
// src/server/plugins.ts
|
|
1563
|
+
import { readWxvpkgFile, readWxvpkgIndex } from "@open-wxa/wxa-runner-extractor";
|
|
1564
|
+
|
|
1565
|
+
// src/server/wechatDev.ts
|
|
1566
|
+
import { TICKET_ENV as TICKET_ENV2 } from "@open-wxa/wxa-runner-extractor";
|
|
1567
|
+
var BASE_URL = "https://servicewechat.com";
|
|
1568
|
+
var CLIENT_VERSION = "1021810240";
|
|
1569
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
1570
|
+
var ERROR_HINTS = {
|
|
1571
|
+
// 40001:开发者工具刷新过 ticket,旧的随即作废
|
|
1572
|
+
40001: "\uFF08\u767B\u5F55\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u91CD\u65B0\u767B\u5F55\uFF09",
|
|
1573
|
+
40029: "\uFF08\u767B\u5F55\u6001\u5DF2\u5931\u6548\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u91CC\u91CD\u65B0\u767B\u5F55\uFF09",
|
|
1574
|
+
85023: "\uFF08\u5F53\u524D\u5FAE\u4FE1\u8D26\u53F7\u4E0D\u662F\u8BE5\u5C0F\u7A0B\u5E8F\u7684\u5F00\u53D1\u8005\uFF09"
|
|
1575
|
+
};
|
|
1576
|
+
function requireUsableTicket(ticket) {
|
|
1577
|
+
if (!ticket) throw new Error(`\u672A\u627E\u5230\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u7684\u767B\u5F55\u6001\uFF0C\u8BF7\u767B\u5F55\u5F00\u53D1\u8005\u5DE5\u5177\uFF0C\u6216\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF ${TICKET_ENV2}`);
|
|
1578
|
+
if (ticket.expiredAt !== void 0 && ticket.expiredAt <= Date.now()) {
|
|
1579
|
+
throw new Error("\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177\u7684\u767B\u5F55\u6001\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u6253\u5F00\u5F00\u53D1\u8005\u5DE5\u5177\u5237\u65B0\u767B\u5F55\u6001");
|
|
1580
|
+
}
|
|
1581
|
+
return ticket.newticket;
|
|
1582
|
+
}
|
|
1583
|
+
function explainErrcode(errcode) {
|
|
1584
|
+
return errcode !== void 0 && ERROR_HINTS[errcode] || "";
|
|
1585
|
+
}
|
|
1586
|
+
function requireOk(body, action) {
|
|
1587
|
+
const errcode = body.baseresponse?.errcode;
|
|
1588
|
+
if (errcode !== 0) throw new Error(`${action} ${errcode}: ${body.baseresponse?.errmsg || "\u672A\u77E5\u9519\u8BEF"}${explainErrcode(errcode)}`);
|
|
1589
|
+
return body;
|
|
1590
|
+
}
|
|
1591
|
+
var WechatDevClient = class {
|
|
1592
|
+
/**
|
|
1593
|
+
* @param findTicket 每次请求都重新取:开发者工具重新登录、切换账号后不用重启服务器
|
|
1594
|
+
*/
|
|
1595
|
+
constructor(appid, findTicket) {
|
|
1596
|
+
this.appid = appid;
|
|
1597
|
+
this.findTicket = findTicket;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* POST JSON 到 path(如 wxa-dev-logic/jslogin),带上 appid 和开发者 ticket
|
|
1601
|
+
*
|
|
1602
|
+
* @throws 没有可用的登录态、超时、连不上、HTTP 状态不是 2xx 时,消息是给用户看的原因
|
|
1603
|
+
*/
|
|
1604
|
+
async post(path7, body, timeout = DEFAULT_TIMEOUT) {
|
|
1605
|
+
const query = new URLSearchParams({ appid: this.appid, newticket: requireUsableTicket(await this.findTicket()), clientversion: CLIENT_VERSION });
|
|
1606
|
+
const response = await fetch(`${BASE_URL}/${path7}?${query}`, {
|
|
1607
|
+
method: "POST",
|
|
1608
|
+
headers: { "Content-Type": "application/json" },
|
|
1609
|
+
body: JSON.stringify(body),
|
|
1610
|
+
signal: AbortSignal.timeout(timeout)
|
|
1611
|
+
}).catch((error) => {
|
|
1612
|
+
throw new Error(error.name === "TimeoutError" ? `\u8BF7\u6C42\u5FAE\u4FE1\u670D\u52A1\u5668\u8D85\u65F6\uFF08${timeout / 1e3}s\uFF09` : `\u8BF7\u6C42\u5FAE\u4FE1\u670D\u52A1\u5668\u5931\u8D25\uFF1A${error.cause?.message ?? error.message}`);
|
|
1613
|
+
});
|
|
1614
|
+
if (!response.ok) throw new Error(`\u5FAE\u4FE1\u670D\u52A1\u5668\u8FD4\u56DE HTTP ${response.status}`);
|
|
1615
|
+
return response;
|
|
1616
|
+
}
|
|
1617
|
+
/** 同 post,返回解析后的 JSON */
|
|
1618
|
+
async postJson(path7, body) {
|
|
1619
|
+
return (await this.post(path7, body)).json();
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
// src/server/plugins.ts
|
|
1624
|
+
var DOWNLOAD_TIMEOUT = 3e4;
|
|
1625
|
+
var PACKAGE_MAGIC = 190;
|
|
1626
|
+
var PluginStore = class {
|
|
1627
|
+
/**
|
|
1628
|
+
* @param wechat 开启 --devtools-login 时才有,没有时不下载插件
|
|
1629
|
+
*/
|
|
1630
|
+
constructor(wechat, logger) {
|
|
1631
|
+
this.wechat = wechat;
|
|
1632
|
+
this.logger = logger;
|
|
1633
|
+
/** provider@version → 下载结果;失败的不留,下次打开小程序时重试 */
|
|
1634
|
+
this.downloads = /* @__PURE__ */ new Map();
|
|
1635
|
+
/**
|
|
1636
|
+
* 这次打开小程序时决定用哪些插件。同一次打开的逻辑层、业务模块、渲染层等请求都按它处理:
|
|
1637
|
+
* 各自去下载的话,下载失败时每个请求都会重试,结果还可能前后不一,页面列表和逻辑层对不上
|
|
1638
|
+
*/
|
|
1639
|
+
this.selection = null;
|
|
1640
|
+
/** 没开 --devtools-login 的提示每个插件只说一次 */
|
|
1641
|
+
this.warned = /* @__PURE__ */ new Set();
|
|
1642
|
+
}
|
|
1643
|
+
/** 打开小程序时(宿主取 app-info)调用:重新决定用哪些插件,下载过的直接用,上次失败的重试 */
|
|
1644
|
+
async refresh(build2) {
|
|
1645
|
+
this.selection = this.select(build2);
|
|
1646
|
+
return withPlugins(build2, await this.selection);
|
|
1647
|
+
}
|
|
1648
|
+
/** 其余请求沿用这次打开时的决定,不触发下载;还没决定过时(如直接请求逻辑层)先决定一次 */
|
|
1649
|
+
async current(build2) {
|
|
1650
|
+
this.selection ??= this.select(build2);
|
|
1651
|
+
return withPlugins(build2, await this.selection);
|
|
1652
|
+
}
|
|
1653
|
+
/** app.json 声明的插件;下载不了的跳过,在终端说明原因,小程序自己的页面照常可用 */
|
|
1654
|
+
async select(build2) {
|
|
1655
|
+
const declared = build2.wxConfig.plugins || {};
|
|
1656
|
+
const results = await Promise.all(Object.entries(declared).map(
|
|
1657
|
+
([alias, { provider, version }]) => provider && version ? this.load(alias, provider, version) : null
|
|
1658
|
+
));
|
|
1659
|
+
return results.filter((plugin) => !!plugin);
|
|
1660
|
+
}
|
|
1661
|
+
async load(alias, provider, version) {
|
|
1662
|
+
const label = `\u63D2\u4EF6 ${alias}\uFF08${provider}@${version}\uFF09`;
|
|
1663
|
+
if (!this.wechat) {
|
|
1664
|
+
this.warnOnce(label, `[wxa-runner] ${label} \u7684\u4EE3\u7801\u8981\u7528\u5F00\u53D1\u8005\u7684\u767B\u5F55\u6001\u5411\u5FAE\u4FE1\u4E0B\u8F7D\uFF0C\u542F\u52A8\u65F6\u52A0 --devtools-login`);
|
|
1665
|
+
return null;
|
|
1666
|
+
}
|
|
1667
|
+
const key = `${provider}@${version}`;
|
|
1668
|
+
let download = this.downloads.get(key);
|
|
1669
|
+
if (!download) {
|
|
1670
|
+
download = this.download(this.wechat, alias, provider, version);
|
|
1671
|
+
this.downloads.set(key, download);
|
|
1672
|
+
download.catch(() => this.downloads.delete(key));
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
return await download;
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
this.logger.warn(`[wxa-runner] ${label} \u4E0B\u8F7D\u5931\u8D25\uFF1A${error.message}`);
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
/** @throws 没有登录态、连不上、微信拒绝或返回的不是插件代码包时,消息是原因 */
|
|
1682
|
+
async download(wechat, alias, provider, version) {
|
|
1683
|
+
const response = await wechat.post("wxa-dev/getplugincode", { plugin_id: provider, plugin_version: version }, DOWNLOAD_TIMEOUT);
|
|
1684
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
1685
|
+
if (data[0] !== PACKAGE_MAGIC) {
|
|
1686
|
+
let body = {};
|
|
1687
|
+
try {
|
|
1688
|
+
body = JSON.parse(data.toString("utf8"));
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
if (body.baseresponse?.errcode) requireOk(body, "\u5FAE\u4FE1\u62D2\u7EDD\u4E0B\u8F7D");
|
|
1692
|
+
throw new Error("\u5FAE\u4FE1\u6CA1\u6709\u8FD4\u56DE\u63D2\u4EF6\u4EE3\u7801");
|
|
1693
|
+
}
|
|
1694
|
+
const files = new Map(readWxvpkgIndex(data).map((entry) => [entry.name, Buffer.from(readWxvpkgFile(data, entry))]));
|
|
1695
|
+
const pluginJson = JSON.parse(files.get("/plugin.json")?.toString("utf8") || "{}");
|
|
1696
|
+
return {
|
|
1697
|
+
alias,
|
|
1698
|
+
provider,
|
|
1699
|
+
version,
|
|
1700
|
+
files,
|
|
1701
|
+
pages: Object.values(pluginJson.pages || {}),
|
|
1702
|
+
pageConfigs: pluginJson.page || {}
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
warnOnce(key, message) {
|
|
1706
|
+
if (this.warned.has(key)) return;
|
|
1707
|
+
this.warned.add(key);
|
|
1708
|
+
this.logger.warn(message);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
|
|
1490
1712
|
// src/server/devtoolsLogin.ts
|
|
1491
1713
|
import express from "express";
|
|
1492
1714
|
import { findDeveloperTicket as findDeveloperTicket2 } from "@open-wxa/wxa-runner-extractor";
|
|
@@ -1553,8 +1775,13 @@ function respond(action) {
|
|
|
1553
1775
|
import fs4 from "fs";
|
|
1554
1776
|
import path4 from "path";
|
|
1555
1777
|
import { build } from "esbuild";
|
|
1556
|
-
|
|
1778
|
+
|
|
1779
|
+
// src/runtime/setupModule.ts
|
|
1557
1780
|
var SETUP_MODULE = "@open-wxa/wxa-runner-core/setup";
|
|
1781
|
+
var SETUP_MODULE_SOURCE = "export function defineSetup(setup) { return setup; }";
|
|
1782
|
+
|
|
1783
|
+
// src/server/setup.ts
|
|
1784
|
+
var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
|
|
1558
1785
|
var ENV_PREFIX = "WXA_";
|
|
1559
1786
|
function findSetupFile(projectPath) {
|
|
1560
1787
|
for (const name of SETUP_FILE_NAMES) {
|
|
@@ -1589,8 +1816,7 @@ function builtinSetupModule() {
|
|
|
1589
1816
|
setup(pluginBuild) {
|
|
1590
1817
|
pluginBuild.onResolve({ filter: new RegExp(`^${SETUP_MODULE}$`) }, () => ({ path: SETUP_MODULE, namespace: "wxa-setup" }));
|
|
1591
1818
|
pluginBuild.onLoad({ filter: /.*/, namespace: "wxa-setup" }, () => ({
|
|
1592
|
-
|
|
1593
|
-
contents: "export function defineSetup(setup) { return setup; }",
|
|
1819
|
+
contents: SETUP_MODULE_SOURCE,
|
|
1594
1820
|
loader: "js"
|
|
1595
1821
|
}));
|
|
1596
1822
|
}
|
|
@@ -1675,16 +1901,7 @@ function applyExtensions(base, extensions, context) {
|
|
|
1675
1901
|
|
|
1676
1902
|
// src/server/DevServer.ts
|
|
1677
1903
|
var RESERVED_PREFIXES = ["/__wxa__", "/__baselib__", "/__plugin__"];
|
|
1678
|
-
var
|
|
1679
|
-
model: "iPhone X",
|
|
1680
|
-
screenWidth: 375,
|
|
1681
|
-
screenHeight: 812,
|
|
1682
|
-
pixelRatio: 3,
|
|
1683
|
-
statusBarHeight: 44,
|
|
1684
|
-
safeAreaBottom: 34,
|
|
1685
|
-
system: "iOS 16.0",
|
|
1686
|
-
platform: "devtools"
|
|
1687
|
-
};
|
|
1904
|
+
var APP_ROUTES = /^\/(?:__wxa__\/(?:appservice\.html|subcontext\.html|worker\.js|service-|page-|webview\/.+\.html$|plugin\/|module\/)|__plugin__\/)/;
|
|
1688
1905
|
var DevServer = class {
|
|
1689
1906
|
constructor(options) {
|
|
1690
1907
|
this.options = options;
|
|
@@ -1820,63 +2037,11 @@ ${error.message}`);
|
|
|
1820
2037
|
res.status(500).type("text").send(error.message);
|
|
1821
2038
|
}
|
|
1822
2039
|
});
|
|
1823
|
-
app.get(
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
res.
|
|
1827
|
-
|
|
1828
|
-
app.get("/__wxa__/subcontext.html", (req, res) => {
|
|
1829
|
-
const id = Number(req.query.id);
|
|
1830
|
-
if (!Number.isInteger(id) || id <= 0) {
|
|
1831
|
-
res.status(400).type("text").send("\u5B50\u4E0A\u4E0B\u6587 id \u65E0\u6548");
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
res.type("html").send(subcontextHtml(id, req.query.empty === "1"));
|
|
1835
|
-
});
|
|
1836
|
-
app.get(/^\/__wxa__\/webview\/(.+)\.html$/, this.withBuild((build2, req, res, next) => {
|
|
1837
|
-
const pagePath = decodeURIComponent(req.params[0]);
|
|
1838
|
-
if (!build2.pages.includes(pagePath)) return next();
|
|
1839
|
-
const pluginPage = findPluginPage(build2, pagePath);
|
|
1840
|
-
const scripts = this.webviewScripts;
|
|
1841
|
-
res.type("html").send(pluginPage ? pluginWebviewHtml(build2, pluginPage.plugin, pluginPage.innerPath, pagePath, Number(req.query.id), { scripts }) : webviewHtml(build2, pagePath, Number(req.query.id), { hot: this.hotEnabled, scripts }));
|
|
1842
|
-
}));
|
|
1843
|
-
app.get("/__wxa__/plugin/:provider/:version/:file(appservice.js|pageframe.js)", this.withBuild((build2, req, res, next) => {
|
|
1844
|
-
const { provider, version, file } = req.params;
|
|
1845
|
-
const code = build2.plugins.find((plugin) => plugin.provider === provider && plugin.version === version)?.files.get(`/${file}`);
|
|
1846
|
-
if (!code) return next();
|
|
1847
|
-
res.type("js").send(code);
|
|
1848
|
-
}));
|
|
1849
|
-
app.get(/^\/__plugin__\/([^/]+)(\/.+)$/, this.withBuild((build2, req, res, next) => {
|
|
1850
|
-
const file = build2.plugins.find((plugin) => plugin.provider === req.params[0])?.files.get(decodeURIComponent(req.params[1]));
|
|
1851
|
-
if (!file) return next();
|
|
1852
|
-
res.type(path6.extname(req.params[1]) || "application/octet-stream").send(file);
|
|
1853
|
-
}));
|
|
1854
|
-
app.get(/^\/__wxa__\/module\/(.+\.js)$/, this.withBuild((build2, req, res) => {
|
|
1855
|
-
const id = decodeURIComponent(req.params[0]);
|
|
1856
|
-
if (!build2.modules.includes(id) && !build2.workerModules.includes(id)) {
|
|
1857
|
-
res.status(404).type("js").send(`console.error(${JSON.stringify(`[wxa-runner] \u6A21\u5757 ${id} \u4E0D\u5B58\u5728`)})`);
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
|
-
res.type("js").send(this.builder.getModuleScript(id));
|
|
1861
|
-
}));
|
|
1862
|
-
app.get("/__wxa__/worker.js", this.withBuild((build2, _req, res) => {
|
|
1863
|
-
const hasWorker = fs6.existsSync(path6.join(this.baseLib.dir, "WAWorker.js"));
|
|
1864
|
-
res.type("js").send(workerScript(build2, { version: this.baseLib.version, hasWorker }));
|
|
1865
|
-
}));
|
|
1866
|
-
app.get("/__wxa__/service-bootstrap.js", this.withBuild((build2, _req, res) => {
|
|
1867
|
-
res.type("js").send(build2.serviceBootstrap);
|
|
1868
|
-
}));
|
|
1869
|
-
app.get("/__wxa__/service-templates.js", this.withBuild((build2, _req, res) => {
|
|
1870
|
-
res.type("js").send(build2.serviceTemplates);
|
|
1871
|
-
}));
|
|
1872
|
-
app.get("/__wxa__/page-templates.js", this.withBuild((build2, _req, res) => {
|
|
1873
|
-
res.type("js").send(build2.pageTemplates);
|
|
1874
|
-
}));
|
|
1875
|
-
app.get("/__wxa__/page-styles.js", this.withBuild((build2, _req, res) => {
|
|
1876
|
-
res.type("js").send(build2.pageStyles);
|
|
1877
|
-
}));
|
|
1878
|
-
app.get("/__wxa__/page-frame.js", this.withBuild((build2, _req, res) => {
|
|
1879
|
-
res.type("js").send(build2.pageFrame);
|
|
2040
|
+
app.get(APP_ROUTES, this.withBuild((build2, req, res, next) => {
|
|
2041
|
+
const response = routeApp(this.appSource(build2), new URL(req.originalUrl, "http://localhost"));
|
|
2042
|
+
if (!response) return next();
|
|
2043
|
+
res.status(response.status).type(response.type);
|
|
2044
|
+
res.send(typeof response.body === "string" ? response.body : Buffer.from(response.body.buffer, response.body.byteOffset, response.body.byteLength));
|
|
1880
2045
|
}));
|
|
1881
2046
|
app.get("/__wxa__/runtime/frame.js.map", (_req, res, next) => {
|
|
1882
2047
|
const file = path6.join(this.runtimeDir(), "frame.js.map");
|
|
@@ -1939,7 +2104,7 @@ ${error.message}`);
|
|
|
1939
2104
|
res.status(400).type("text").send(`\u975E\u6CD5\u7684\u4F1A\u8BDD\u6807\u8BC6 ${req.params.session}`);
|
|
1940
2105
|
return;
|
|
1941
2106
|
}
|
|
1942
|
-
const filePath = `/__wxa__/tmp/${req.params.session}/${
|
|
2107
|
+
const filePath = `/__wxa__/tmp/${req.params.session}/${randomUUID2()}.${type}`;
|
|
1943
2108
|
files.write(filePath, Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0));
|
|
1944
2109
|
res.json({ url: `${req.protocol}://${req.get("host")}${filePath}` });
|
|
1945
2110
|
});
|
|
@@ -1959,7 +2124,7 @@ ${error.message}`);
|
|
|
1959
2124
|
res.status(404).end();
|
|
1960
2125
|
return;
|
|
1961
2126
|
}
|
|
1962
|
-
const data = node.data;
|
|
2127
|
+
const data = Buffer.from(node.data.buffer, node.data.byteOffset, node.data.byteLength);
|
|
1963
2128
|
res.type(path6.extname(req.params[0]));
|
|
1964
2129
|
res.setHeader("ETag", `"${node.etag}"`);
|
|
1965
2130
|
res.setHeader("Accept-Ranges", "bytes");
|
|
@@ -2010,6 +2175,16 @@ ${error.message}`);
|
|
|
2010
2175
|
return this.plugins.current(await this.builder.build());
|
|
2011
2176
|
}
|
|
2012
2177
|
/** 需要构建结果的路由;构建失败时返回错误文本 */
|
|
2178
|
+
appSource(build2) {
|
|
2179
|
+
return {
|
|
2180
|
+
build: build2,
|
|
2181
|
+
moduleScript: (id) => this.builder.getModuleScript(id),
|
|
2182
|
+
baseLib: { version: this.baseLib.version, hasWorker: fs6.existsSync(path6.join(this.baseLib.dir, "WAWorker.js")) },
|
|
2183
|
+
isolated: usesIsolatedContext(this.baseLib.version),
|
|
2184
|
+
hot: this.hotEnabled,
|
|
2185
|
+
webviewScripts: this.webviewScripts
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2013
2188
|
withBuild(handler) {
|
|
2014
2189
|
return async (req, res, next) => {
|
|
2015
2190
|
res.setHeader("Cache-Control", "no-store");
|
|
@@ -2028,17 +2203,7 @@ ${error.message}`);
|
|
|
2028
2203
|
};
|
|
2029
2204
|
}
|
|
2030
2205
|
runtimeDir() {
|
|
2031
|
-
|
|
2032
|
-
this.options.runtimeDir,
|
|
2033
|
-
path6.join(__dirname, "runtime"),
|
|
2034
|
-
path6.join(__dirname, "../runtime"),
|
|
2035
|
-
path6.join(__dirname, "../../dist/runtime")
|
|
2036
|
-
].filter((dir) => !!dir);
|
|
2037
|
-
const found = candidates.find((dir) => fs6.existsSync(path6.join(dir, "host.js")));
|
|
2038
|
-
if (!found) {
|
|
2039
|
-
throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
|
|
2040
|
-
}
|
|
2041
|
-
return found;
|
|
2206
|
+
return findRuntimeDir(this.options.runtimeDir);
|
|
2042
2207
|
}
|
|
2043
2208
|
/** 只在监听文件变化时打开基础库的热更新开关,跑测试(watch: false)时运行环境与真机一致 */
|
|
2044
2209
|
get hotEnabled() {
|
|
@@ -2126,11 +2291,30 @@ function resolveSetupFile(option, projectPath) {
|
|
|
2126
2291
|
if (!fs6.existsSync(file)) throw new Error(`\u627E\u4E0D\u5230 setup \u6587\u4EF6\uFF1A${file}`);
|
|
2127
2292
|
return file;
|
|
2128
2293
|
}
|
|
2294
|
+
function findRuntimeDir(explicit) {
|
|
2295
|
+
const candidates = [
|
|
2296
|
+
explicit,
|
|
2297
|
+
path6.join(__dirname, "runtime"),
|
|
2298
|
+
path6.join(__dirname, "../runtime"),
|
|
2299
|
+
path6.join(__dirname, "../../dist/runtime")
|
|
2300
|
+
].filter((dir) => !!dir);
|
|
2301
|
+
const found = candidates.find((dir) => fs6.existsSync(path6.join(dir, "host.js")));
|
|
2302
|
+
if (!found) {
|
|
2303
|
+
throw new Error("\u627E\u4E0D\u5230\u6D4F\u89C8\u5668\u7AEF\u8FD0\u884C\u65F6\uFF08host.js\uFF09\uFF0C\u8BF7\u5148\u6267\u884C pnpm build");
|
|
2304
|
+
}
|
|
2305
|
+
return found;
|
|
2306
|
+
}
|
|
2129
2307
|
export {
|
|
2130
2308
|
AppBuilder,
|
|
2131
2309
|
DEFAULT_DEVICE,
|
|
2132
2310
|
DevServer,
|
|
2311
|
+
LOCAL_HOST_OF_ANY_ADDRESS,
|
|
2133
2312
|
MiniProgram,
|
|
2134
|
-
|
|
2313
|
+
bundleSetup,
|
|
2314
|
+
createWxConfig,
|
|
2315
|
+
findRuntimeDir,
|
|
2316
|
+
findSetupFile,
|
|
2317
|
+
handleProxy,
|
|
2318
|
+
listen
|
|
2135
2319
|
};
|
|
2136
2320
|
//# sourceMappingURL=index.mjs.map
|