@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.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 randomUUID3 } from "crypto";
6
+ import { randomUUID as randomUUID2 } from "crypto";
7
7
 
8
8
  // src/server/FileStore.ts
9
- import { randomUUID } from "crypto";
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: Buffer.alloc(0), mtime: Date.now(), etag: "" }]]);
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.toString("base64"), binary: true } : { data: data.toString(encoding) };
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: Buffer.alloc(0), mtime: Date.now(), etag: "" });
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 = Buffer.from(input);
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
- const contents = append && existing ? Buffer.concat([existing.data, data]) : data;
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, Buffer.from(args.data ?? "", args.binary ? "base64" : args.encoding || "utf8"), append);
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/plugins.ts
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 page = plugin.files.get(`/${innerPath}.html`)?.toString("utf8") ?? `<script>console.error(${JSON.stringify(`[wxa-runner] \u63D2\u4EF6 ${plugin.alias} \u91CC\u6CA1\u6709\u9875\u9762 ${innerPath}`)})</script>`;
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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
- async function handleProxy(req, res) {
1266
- const target = fromServerPath(PROXY_PREFIX, req.originalUrl);
1267
- if (!target) {
1268
- sendProxyError(res, 400, `invalid url "${req.originalUrl}"`);
1269
- return;
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
- try {
1277
- upstream = await fetch(target, {
1278
- method: req.method,
1279
- headers: forwardedRequestHeaders(req.get("x-wxa-headers"), req.get("content-type")),
1280
- body: requestBody(req),
1281
- redirect: "follow",
1282
- signal: controller.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
- } catch (error) {
1285
- if (res.destroyed || controller.signal.aborted) return;
1286
- sendProxyError(res, 502, fetchErrorMessage(error));
1287
- return;
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
- await relayResponse(upstream, res);
1329
+ return relayResponse(upstream);
1290
1330
  } catch (error) {
1291
- if (!res.destroyed) res.destroy(error);
1292
- } finally {
1293
- res.off("close", cancel);
1331
+ return proxyError(502, fetchErrorMessage(error));
1294
1332
  }
1295
1333
  }
1296
- function requestBody(req) {
1297
- const hasBody = !["GET", "HEAD"].includes(req.method) && Buffer.isBuffer(req.body) && req.body.length > 0;
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 fetchErrorMessage(error) {
1301
- return error?.cause?.message || error.message;
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 sendProxyError(res, status, message) {
1304
- res.status(status).set("x-wxa-proxy-error", "1").send(message);
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
- return JSON.parse(encoded);
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
- async function relayResponse(upstream, res) {
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)) res.setHeader(key, value);
1368
+ if (!DROPPED_RESPONSE_HEADERS.has(key)) headers.set(key, value);
1329
1369
  });
1330
- res.status(upstream.status);
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
- res.setHeader("x-wxa-set-cookie", encodeURIComponent(JSON.stringify(setCookies)));
1334
- res.flushHeaders();
1335
- if (!upstream.body) {
1336
- res.end();
1337
- return;
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 as randomUUID2 } from "crypto";
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 as TICKET_ENV2 } from "@open-wxa/wxa-runner-extractor";
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[TICKET_ENV2]) return { newticket: process.env[TICKET_ENV2] };
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-${randomUUID2()}`;
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
- var SETUP_FILE_NAMES = ["wxa.setup.ts", "wxa.setup.js", "wxa.setup.mjs"];
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
- // 与 src/setup.ts 的 defineSetup 一致:只为类型提示,运行时原样返回
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 DEFAULT_DEVICE = {
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("/__wxa__/appservice.html", this.withBuild((build2, req, res) => {
1824
- const launchPath = String(req.query.path || "").replace(/^\//, "");
1825
- const launch = build2.pages.includes(launchPath) ? { path: launchPath, query: Object.fromEntries(new URLSearchParams(String(req.query.query || ""))) } : { path: build2.pages[0], query: {} };
1826
- res.type("html").send(appserviceHtml(build2, launch, { hot: this.hotEnabled, isolated: usesIsolatedContext(this.baseLib.version) }));
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}/${randomUUID3()}.${type}`;
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
- const candidates = [
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
- createWxConfig
2313
+ bundleSetup,
2314
+ createWxConfig,
2315
+ findRuntimeDir,
2316
+ findSetupFile,
2317
+ handleProxy,
2318
+ listen
2135
2319
  };
2136
2320
  //# sourceMappingURL=index.mjs.map