@open-wxa/wxa-runner-core 0.12.3 → 0.13.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.
@@ -1,11 +1,52 @@
1
1
  "use strict";
2
2
  (() => {
3
- // src/runtime/host/ConsoleForwarder.ts
3
+ // src/runtime/host/frameErrors.ts
4
4
  var DEDUPE_MS = 1e3;
5
+ var MINI_PROGRAM_ERROR = /^(\(in promise\) )?MiniProgramError\n/;
6
+ var STACK_FRAME = /^\s*at /;
7
+ function isErrorLike(value) {
8
+ return typeof value === "object" && value !== null && typeof value.message === "string";
9
+ }
10
+ function caughtError(value) {
11
+ if (isErrorLike(value) && MINI_PROGRAM_ERROR.test(value.message)) {
12
+ const ownStack = typeof value.stack === "string" && /^[^\n]*MiniProgramError\n/.test(value.stack);
13
+ return { message: value.message.replace(MINI_PROGRAM_ERROR, ""), stack: ownStack ? void 0 : value.stack };
14
+ }
15
+ if (typeof value === "string" && MINI_PROGRAM_ERROR.test(value)) {
16
+ const text = value.replace(MINI_PROGRAM_ERROR, "");
17
+ const firstFrame2 = text.search(/^\s*at /m);
18
+ const head = (firstFrame2 < 0 ? text : text.slice(0, firstFrame2)).replace(/\n$/, "");
19
+ const message = /^([\s\S]*)\n[^\n]*?: \1$/.exec(head)?.[1] ?? head;
20
+ return { message, stack: firstFrame2 < 0 ? void 0 : text.slice(firstFrame2) };
21
+ }
22
+ return null;
23
+ }
24
+ var RecentErrors = class {
25
+ constructor() {
26
+ this.recent = /* @__PURE__ */ new Map();
27
+ }
28
+ /**
29
+ * 比较栈里的各帧而不是整个栈:基础库打印成字符串时,栈开头的 `TypeError: …` 那行拆不出来。
30
+ * 不同位置抛出的同名错误帧不同,各记各的
31
+ */
32
+ isRepeated(kind, webviewId, message, stack) {
33
+ const frames = (stack || "").split("\n").filter((line) => STACK_FRAME.test(line)).map((line) => line.trim()).join("\n");
34
+ const key = `${kind}:${webviewId}:${message}
35
+ ${frames}`;
36
+ const now = Date.now();
37
+ for (const [recent, time] of this.recent) {
38
+ if (now - time >= DEDUPE_MS) this.recent.delete(recent);
39
+ }
40
+ if (this.recent.has(key)) return true;
41
+ this.recent.set(key, now);
42
+ return false;
43
+ }
44
+ };
45
+
46
+ // src/runtime/host/ConsoleForwarder.ts
5
47
  var MAX_ARG_LENGTH = 1e3;
6
48
  var MAX_TEXT_LENGTH = 8e3;
7
49
  var LAYERS = { service: "\u903B\u8F91\u5C42", webview: "\u6E32\u67D3\u5C42" };
8
- var MINI_PROGRAM_ERROR = /^(\(in promise\) )?MiniProgramError\n/;
9
50
  var MODULE_URL = /\bhttps?:\/\/[^/\s]+\/__wxa__\/module\/([^?\s)]+?)(?:\?[^:\s)]*)?:(\d+):(\d+)/;
10
51
  var ConsoleForwarder = class {
11
52
  /**
@@ -17,13 +58,11 @@
17
58
  this.send = send;
18
59
  this.pageOf = pageOf;
19
60
  this.hintsOf = hintsOf;
20
- this.recentErrors = /* @__PURE__ */ new Map();
21
61
  }
22
- /** 运行时错误:全局 error、未处理的 Promise 拒绝、基础库捕获的业务代码错误 */
62
+ /** 运行时错误:全局 error、未处理的 Promise 拒绝、基础库捕获的业务代码错误。两路报来的同一个错误由宿主去重(见 frameErrors.ts) */
23
63
  error(kind, webviewId, message, stack) {
64
+ if (!this.mode) return;
24
65
  const frames = businessFrames(stack);
25
- if (!this.mode || this.isRepeated(`${kind}:${webviewId}:${message}
26
- ${frames}`)) return;
27
66
  const hints = this.hintsOf(kind).map((hint) => `
28
67
  \u63D0\u793A\uFF1A${hint}`).join("");
29
68
  this.post("error", `${this.origin(kind, webviewId)}\u62A5\u9519\uFF1A${message}${frames ? `
@@ -33,14 +72,9 @@ ${frames}` : ""}${hints}`);
33
72
  warn(text) {
34
73
  if (this.mode) this.post("warn", text);
35
74
  }
36
- /** iframe 里的 console.warn / console.error;基础库捕获业务代码错误后打印的 console.error 按报错转发 */
75
+ /** iframe 里的 console.warn / console.error;基础库捕获业务代码错误后打印的不经过这里,按报错转发 */
37
76
  console(kind, webviewId, level, args) {
38
- const thrown = level === "error" ? miniProgramError(args[0]) : null;
39
- if (thrown) {
40
- this.error(kind, webviewId, thrown.message, thrown.stack);
41
- } else if (this.mode === "all") {
42
- this.post(level, `${this.origin(kind, webviewId)} console.${level}\uFF1A${args.map(formatArg).join(" ")}`);
43
- }
77
+ if (this.mode === "all") this.post(level, `${this.origin(kind, webviewId)} console.${level}\uFF1A${args.map(formatArg).join(" ")}`);
44
78
  }
45
79
  post(level, text) {
46
80
  this.send({ type: "console", level, text: text.length > MAX_TEXT_LENGTH ? `${text.slice(0, MAX_TEXT_LENGTH)}\u2026` : text });
@@ -49,33 +83,7 @@ ${frames}` : ""}${hints}`);
49
83
  const page = this.pageOf(kind, webviewId);
50
84
  return page ? `${LAYERS[kind]}\uFF08${page}\uFF09` : LAYERS[kind];
51
85
  }
52
- /** @param key 来源、错误信息和业务代码的帧:两路报来的同一个错误相同,不同位置抛出的同名错误不同 */
53
- isRepeated(key) {
54
- const now = Date.now();
55
- for (const [recent, time] of this.recentErrors) {
56
- if (now - time >= DEDUPE_MS) this.recentErrors.delete(recent);
57
- }
58
- if (this.recentErrors.has(key)) return true;
59
- this.recentErrors.set(key, now);
60
- return false;
61
- }
62
86
  };
63
- function isErrorLike(value) {
64
- return typeof value === "object" && value !== null && typeof value.message === "string";
65
- }
66
- function miniProgramError(value) {
67
- if (isErrorLike(value) && MINI_PROGRAM_ERROR.test(value.message)) {
68
- return { message: value.message.replace(MINI_PROGRAM_ERROR, ""), stack: value.stack };
69
- }
70
- if (typeof value === "string" && MINI_PROGRAM_ERROR.test(value)) {
71
- const text = value.replace(MINI_PROGRAM_ERROR, "");
72
- const firstFrame2 = text.search(/^\s*at /m);
73
- const head = (firstFrame2 < 0 ? text : text.slice(0, firstFrame2)).replace(/\n$/, "");
74
- const message = /^([\s\S]*)\n[^\n]*?: \1$/.exec(head)?.[1] ?? head;
75
- return { message, stack: firstFrame2 < 0 ? void 0 : text.slice(firstFrame2) };
76
- }
77
- return null;
78
- }
79
87
  function businessFrames(stack) {
80
88
  return (stack || "").split("\n").filter((line) => /^\s*at /.test(line) && MODULE_URL.test(line)).map((line) => ` ${line.trim().replace(MODULE_URL, "$1:$2:$3")}`).join("\n");
81
89
  }
@@ -106,7 +114,6 @@ ${frames}` : "");
106
114
  "systemLog",
107
115
  "traceEvent",
108
116
  "getPermissionBytes",
109
- "getABTestConfig",
110
117
  "initRenderDone",
111
118
  "saveInitialRenderingCache",
112
119
  "setKeyValue",
@@ -627,6 +634,7 @@ ${frames}` : "");
627
634
 
628
635
  // src/runtime/host/api/misc.ts
629
636
  var SERVICE_MARKET = "webapi_servicemarket_jsapi_comm";
637
+ var CLOUD = "qbase_commapi";
630
638
  function createMiscApis(appid, options) {
631
639
  const viaDevtools = async (api, url, json) => {
632
640
  try {
@@ -636,6 +644,14 @@ ${frames}` : "");
636
644
  throw error;
637
645
  }
638
646
  };
647
+ let cloudWarned = false;
648
+ const cloudUnavailable = (api, operation) => {
649
+ if (!cloudWarned) {
650
+ cloudWarned = true;
651
+ console.warn("[wxa-runner] \u4E91\u5F00\u53D1\uFF08wx.cloud\uFF09\u5728\u6D4F\u89C8\u5668\u8FD0\u884C\u65F6\u4E0D\u53EF\u7528\uFF0C\u8C03\u7528\u4F1A\u5931\u8D25");
652
+ }
653
+ return fail(api, `wxa-runner \u4E0D\u652F\u6301\u4E91\u5F00\u53D1\uFF08${operation ?? "unknown"}\uFF09`);
654
+ };
639
655
  return {
640
656
  // canvas 导出图片、拍照、录像的结果都经过这里
641
657
  base64ToTempFilePath: async (args) => {
@@ -652,11 +668,14 @@ ${frames}` : "");
652
668
  (error) => serviceMarketFail(error.message)
653
669
  );
654
670
  }
671
+ if (args.data?.api_name === CLOUD) return cloudUnavailable(api, args.data.data?.qbase_api_name);
655
672
  if (args.data?.api_name === "webapi_getwxanetwork") return bizDomainUnavailable(api);
656
673
  if (args.data?.api_name === "webapi_getwxauserprivacyauthinfo") return ok(api, { data: { data: JSON.stringify(PRIVACY_AUTH_INFO) } });
657
- return ok(api);
674
+ return ok(api, { data: {} });
658
675
  },
659
676
  getAppContact: (args, { api }) => args.keyPath === "WxaAppInfo.Network.BizDomain" ? bizDomainUnavailable(api) : ok(api),
677
+ // 基础库内部的实验开关。不在实验里时原生返回空字符串;没有 testConfig 时 3.3.4 读它的 length 报错
678
+ getABTestConfig: (_args, { api }) => ok(api, { testConfig: "" }),
660
679
  // 插件:基础库启动时校验插件、取插件信息,插件里的网络请求要等拿到这两样才发出。插件代码也只在 --devtools-login 时下载
661
680
  ...options?.devtoolsLogin ? {
662
681
  verifyPlugin: (async (args, { api }) => ok(api, await viaDevtools(api, "/__wxa__/api/verify-plugin", { data: args.data }))),
@@ -1958,7 +1977,7 @@ ${frames}` : "");
1958
1977
  if (warnedOrigins.has(url.origin)) return;
1959
1978
  warnedOrigins.add(url.origin);
1960
1979
  console.warn(
1961
- `[wxa-runner] web-view \u52A0\u8F7D\u7684 ${url.origin} \u4E0E\u6A21\u62DF\u5668\uFF08${location.origin}\uFF09\u4E0D\u662F\u540C\u4E00\u4E2A\u7AD9\u70B9\uFF0C\u6D4F\u89C8\u5668\u6309\u8DE8\u7AD9\u5D4C\u5165\u5904\u7406\uFF0C\u7F51\u9875\u7684 Cookie \u53EF\u80FD\u4E0D\u751F\u6548\uFF08\u5FAE\u4FE1\u91CC\u7684\u5C0F\u7A0B\u5E8F\u6CA1\u6709\u8FD9\u4E2A\u95EE\u9898\uFF09\u3002\u9700\u8981 Cookie \u65F6\u8BA9\u7F51\u9875\u5728\u65B0\u7A97\u53E3\u6253\u5F00\uFF1A\u547D\u4EE4\u884C\u52A0 --webview-window\uFF0CPlaywright \u7528 test.use({ wxaWebView: 'window' })`
1980
+ `[wxa-runner] web-view \u52A0\u8F7D\u7684 ${url.origin} \u4E0E\u6A21\u62DF\u5668\uFF08${location.origin}\uFF09\u4E0D\u662F\u540C\u4E00\u4E2A\u7AD9\u70B9\uFF0C\u6D4F\u89C8\u5668\u6309\u8DE8\u7AD9\u5D4C\u5165\u5904\u7406\uFF0C\u7F51\u9875\u7684 Cookie \u53EF\u80FD\u4E0D\u751F\u6548\uFF08\u5FAE\u4FE1\u91CC\u7684\u5C0F\u7A0B\u5E8F\u6CA1\u6709\u8FD9\u4E2A\u95EE\u9898\uFF09\u3002\u9700\u8981 Cookie \u65F6\u8BA9\u7F51\u9875\u5728\u65B0\u7A97\u53E3\u6253\u5F00\uFF1A\u914D\u7F6E\u6587\u4EF6 wxa.config.ts \u91CC\u5199 webViewWindow: true\uFF0CPlaywright \u7528 test.use({ wxaWebView: 'window' })`
1962
1981
  );
1963
1982
  }
1964
1983
  function readableUrl(frame) {
@@ -2138,6 +2157,49 @@ ${frames}` : "");
2138
2157
  return navigator.userActivation?.isActive ?? true;
2139
2158
  }
2140
2159
 
2160
+ // src/runtime/host/ui/pageTransition.ts
2161
+ var DURATION = 300;
2162
+ function canAnimatePageTransition(frame) {
2163
+ return frame.isConnected && !document.hidden && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2164
+ }
2165
+ async function animatePageTransition(previous, current, direction) {
2166
+ const viewport = current.parentElement;
2167
+ const body = current.contentDocument?.body;
2168
+ if (!viewport || previous.parentElement !== viewport || !body) return;
2169
+ const { visibility, zIndex } = previous.style;
2170
+ const outgoing = previous.contentDocument?.documentElement ?? previous;
2171
+ const wasInert = outgoing.inert;
2172
+ const shield = body.ownerDocument.createElement("div");
2173
+ shield.className = "wxa-page-transition-shield";
2174
+ shield.setAttribute("aria-hidden", "true");
2175
+ shield.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; pointer-events: auto; background: transparent;";
2176
+ const sliding = direction === "push" ? current : previous;
2177
+ const transform = direction === "push" ? ["translateX(100%)", "translateX(0)"] : ["translateX(0)", "translateX(100%)"];
2178
+ let animation;
2179
+ let timer;
2180
+ try {
2181
+ outgoing.inert = true;
2182
+ body.append(shield);
2183
+ previous.style.visibility = "visible";
2184
+ if (direction === "pop") previous.style.zIndex = "2";
2185
+ animation = sliding.animate({ transform }, { duration: DURATION, easing: "ease", fill: "both" });
2186
+ await Promise.race([
2187
+ animation.finished.catch(() => void 0),
2188
+ // 切到后台后,浏览器可能暂停动画,路由队列仍需继续。
2189
+ new Promise((resolve) => {
2190
+ timer = setTimeout(resolve, DURATION + 60);
2191
+ })
2192
+ ]);
2193
+ } finally {
2194
+ clearTimeout(timer);
2195
+ animation?.cancel();
2196
+ previous.style.visibility = visibility;
2197
+ previous.style.zIndex = zIndex;
2198
+ outgoing.inert = wasInert;
2199
+ shield.remove();
2200
+ }
2201
+ }
2202
+
2141
2203
  // src/runtime/host/ui/dom.ts
2142
2204
  function element(tag, className, text) {
2143
2205
  const el = document.createElement(tag);
@@ -2456,6 +2518,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2456
2518
  this.pages = root.querySelector(".wxa-pages");
2457
2519
  this.tabBar = root.querySelector(".wxa-tabbar");
2458
2520
  this.overlay = root.querySelector(".wxa-overlay");
2521
+ guardOverlayTouchClicks(this.overlay);
2459
2522
  this.routeLabel = root.querySelector(".wxa-route");
2460
2523
  root.querySelector(".wxa-reload").addEventListener("click", () => location.reload());
2461
2524
  this.scaleButton.addEventListener("click", () => {
@@ -2494,6 +2557,18 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2494
2557
  onBack(handler) {
2495
2558
  this.backButton.addEventListener("click", handler);
2496
2559
  }
2560
+ async transitionPages(previous, current, direction) {
2561
+ const navWasInert = this.navBar.inert;
2562
+ const tabsWereInert = this.tabBar.inert;
2563
+ this.navBar.inert = true;
2564
+ this.tabBar.inert = true;
2565
+ try {
2566
+ await animatePageTransition(previous, current, direction);
2567
+ } finally {
2568
+ this.navBar.inert = navWasInert;
2569
+ this.tabBar.inert = tabsWereInert;
2570
+ }
2571
+ }
2497
2572
  setTabBar(config, onTap) {
2498
2573
  this.tabBarConfig = config && config.list?.length && !config.custom ? config : null;
2499
2574
  this.onTabTap = onTap;
@@ -2738,6 +2813,24 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2738
2813
  function maskElement(dim) {
2739
2814
  return element("div", "wxa-mask" + (dim ? " is-dim" : ""));
2740
2815
  }
2816
+ function guardOverlayTouchClicks(overlay) {
2817
+ let press;
2818
+ overlay.addEventListener("pointerdown", (event) => {
2819
+ press = event.pointerType === "touch" && event.target instanceof Node ? { id: event.pointerId, target: event.target } : void 0;
2820
+ }, true);
2821
+ overlay.addEventListener("pointercancel", (event) => {
2822
+ if (press?.id === event.pointerId) press = void 0;
2823
+ }, true);
2824
+ overlay.addEventListener("click", (event) => {
2825
+ if (event.pointerType !== "touch") return;
2826
+ if (press?.id === event.pointerId) {
2827
+ const start = press;
2828
+ press = void 0;
2829
+ if (event.target instanceof Node && event.target.contains(start.target)) return;
2830
+ }
2831
+ event.stopPropagation();
2832
+ }, true);
2833
+ }
2741
2834
  var BACK_ICON = '<svg width="12" height="20" viewBox="0 0 12 20"><path d="M10 2 2 10l8 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
2742
2835
  var MORE_ICON = '<svg width="22" height="8" viewBox="0 0 22 8" fill="currentColor"><circle cx="3.5" cy="4" r="2.3"/><circle cx="11" cy="4" r="3.3"/><circle cx="18.5" cy="4" r="2.3"/></svg>';
2743
2836
  var CLOSE_ICON = '<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8.2" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="10" cy="10" r="3.4" fill="currentColor"/></svg>';
@@ -2760,6 +2853,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2760
2853
  constructor(info, root, sendReport = () => void 0) {
2761
2854
  this.info = info;
2762
2855
  this.errors = [];
2856
+ this.recentErrors = new RecentErrors();
2763
2857
  /** 小程序发出的 wx.request */
2764
2858
  this.requests = new RequestLog();
2765
2859
  this.api = new ApiRegistry((context) => this.reportUnimplemented(context));
@@ -2917,14 +3011,20 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2917
3011
  message: err ? err.message : String(error),
2918
3012
  stack: typeof err?.stack === "string" ? err.stack : void 0
2919
3013
  };
2920
- this.errors.push(report);
2921
- this.forwarder.error(kind, webviewId, report.message, report.stack);
3014
+ this.recordError(report);
2922
3015
  }
2923
3016
  get forwardConsole() {
2924
3017
  return this.forwarder.mode;
2925
3018
  }
2926
3019
  reportConsole(kind, webviewId, level, args) {
2927
- this.forwarder.console(kind, webviewId, level, args);
3020
+ const caught = level === "error" ? caughtError(args[0]) : null;
3021
+ if (caught) this.recordError({ kind, webviewId, ...caught });
3022
+ else this.forwarder.console(kind, webviewId, level, args);
3023
+ }
3024
+ recordError(report) {
3025
+ if (this.recentErrors.isRepeated(report.kind, report.webviewId, report.message, report.stack)) return;
3026
+ this.errors.push(report);
3027
+ this.forwarder.error(report.kind, report.webviewId, report.message, report.stack);
2928
3028
  }
2929
3029
  /**
2930
3030
  * 只记录逻辑层的公开 wx API 和 FileSystemManager 方法,每个原生接口一次。
@@ -2987,20 +3087,35 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2987
3087
  this.stack.reset(page);
2988
3088
  this.activate(page, "appLaunch");
2989
3089
  }
2990
- async navigateTo(url) {
3090
+ async navigateTo(url, { animate = true } = {}) {
2991
3091
  if (!this.stack.canPush()) return fail("navigateTo", "webview count limit exceed");
2992
3092
  const { path, query } = this.resolveUrl(url);
2993
3093
  if (this.tabPaths.has(path)) return fail("navigateTo", "can not navigateTo a tabbar page");
2994
3094
  if (!this.hasPage(path)) return fail("navigateTo", `page "${path}" is not found`);
2995
3095
  const page = await this.openPage(path, query);
3096
+ const previous = this.currentPage();
2996
3097
  this.stack.push(page);
2997
3098
  this.activate(page, "navigateTo");
3099
+ if (animate && this.info.pageTransition && previous && canAnimatePageTransition(previous.frame)) {
3100
+ await this.shell.transitionPages(previous.frame, page.frame, "push");
3101
+ }
2998
3102
  return ok("navigateTo", { webviewId: page.id });
2999
3103
  }
3000
3104
  async navigateBack(delta) {
3001
3105
  if (this.stack.size <= 1) return fail("navigateBack", "cannot navigate back at first page.");
3002
- this.stack.pop(delta).forEach((page) => this.closePage(page));
3003
- this.activate(this.currentPage(), "navigateBack");
3106
+ const popped = this.stack.pop(delta);
3107
+ const outgoing = popped[0];
3108
+ const animate = this.info.pageTransition && outgoing && canAnimatePageTransition(outgoing.frame);
3109
+ for (const page of popped) {
3110
+ if (!animate || page !== outgoing) this.closePage(page);
3111
+ }
3112
+ const target = this.currentPage();
3113
+ try {
3114
+ this.activate(target, "navigateBack");
3115
+ if (animate) await this.shell.transitionPages(outgoing.frame, target.frame, "pop");
3116
+ } finally {
3117
+ if (animate) this.closePage(outgoing);
3118
+ }
3004
3119
  return ok("navigateBack");
3005
3120
  }
3006
3121
  async redirectTo(url) {
@@ -3432,7 +3547,7 @@ ${script}
3432
3547
  await host.enqueueRoute(async () => {
3433
3548
  for (const { path, query } of this.saved.slice(1)) {
3434
3549
  const search = new URLSearchParams(query).toString();
3435
- const result = await host.navigateTo(search ? `${path}?${search}` : path);
3550
+ const result = await host.navigateTo(search ? `${path}?${search}` : path, { animate: false });
3436
3551
  if (!String(result.errMsg).endsWith(":ok")) {
3437
3552
  console.warn(`[wxa-runner] \u6062\u590D\u9875\u9762\u6808\u65F6\u6253\u4E0D\u5F00 ${path}\uFF1A${result.errMsg}`);
3438
3553
  return;