@open-wxa/wxa-runner-core 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -236,6 +236,14 @@
236
236
  };
237
237
  function createMiscApis(appid, options) {
238
238
  const fs = new MemoryFileSystem();
239
+ const realLogin = async (api, url) => {
240
+ try {
241
+ return await postDevServer(url);
242
+ } catch (error) {
243
+ options?.onRealLoginFail?.(api, error.message);
244
+ throw error;
245
+ }
246
+ };
239
247
  const guard = (api, action) => {
240
248
  try {
241
249
  return ok(api, action() || {});
@@ -281,13 +289,13 @@
281
289
  const bytes = Uint8Array.from(atob(String(args.base64Data || "")), (char) => char.charCodeAt(0));
282
290
  return { tempFilePath: await saveTempFile(bytes, String(args.fileType || "png")) };
283
291
  },
284
- // 基础库经这里向后台拉各种配置。业务域名列表(webapi_getwxanetwork)要明确失败:
285
- // 拿到空列表时基础库会把 web-view 事件里的网页地址清空,拉取失败才不校验,等同开发者工具的「不校验业务域名」
286
- operateWXData: (args, { api }) => args.data?.api_name === "webapi_getwxanetwork" ? fail(api, "not available in wxa-runner") : ok(api),
292
+ // 基础库经这两个接口向后台拉各种配置和小程序信息
293
+ operateWXData: (args, { api }) => args.data?.api_name === "webapi_getwxanetwork" ? bizDomainUnavailable(api) : ok(api),
294
+ getAppContact: (args, { api }) => args.keyPath === "WxaAppInfo.Network.BizDomain" ? bizDomainUnavailable(api) : ok(api),
287
295
  // 真实登录失败时明确返回 fail,不退回假数据:配置或登录态的问题要让开发者马上发现
288
- login: options?.realLogin ? async (_args, { api }) => ok(api, { code: (await postDevServer("/__wxa__/api/login")).code }) : (_args, { api }) => ok(api, { code: `wxa_mock_code_${Date.now().toString(36)}` }),
296
+ login: options?.realLogin ? async (_args, { api }) => ok(api, { code: (await realLogin(api, "/__wxa__/api/login")).code }) : (_args, { api }) => ok(api, { code: `wxa_mock_code_${Date.now().toString(36)}` }),
289
297
  // 基础库只从 JSON.parse(data) 里取手机号的 code
290
- getPhoneNumber: options?.realLogin ? async (_args, { api }) => ok(api, await postDevServer("/__wxa__/api/phone-number")) : (_args, { api }) => ok(api, { data: JSON.stringify({ code: `mock_phone_code_${Date.now()}` }), encryptedData: "mock_encrypted", iv: "mock_iv" }),
298
+ getPhoneNumber: options?.realLogin ? async (_args, { api }) => ok(api, await realLogin(api, "/__wxa__/api/phone-number")) : (_args, { api }) => ok(api, { data: JSON.stringify({ code: `mock_phone_code_${Date.now()}` }), encryptedData: "mock_encrypted", iv: "mock_iv" }),
291
299
  checkSession: () => void 0,
292
300
  authorize: () => void 0,
293
301
  getSetting: (_args, { api }) => ok(api, {
@@ -321,6 +329,9 @@
321
329
  function mockUser() {
322
330
  return { nickName: "wxa-runner", avatarUrl: "", gender: 0, country: "", province: "", city: "", language: "zh_CN" };
323
331
  }
332
+ function bizDomainUnavailable(api) {
333
+ return fail(api, "not available in wxa-runner");
334
+ }
324
335
 
325
336
  // src/runtime/host/api/network.ts
326
337
  var RequestLog = class {
@@ -479,20 +490,26 @@
479
490
  }
480
491
  return ok(api);
481
492
  };
482
- return {
493
+ const createTask = {
483
494
  createRequestTask,
484
495
  createRequestTaskAsync: createRequestTask,
485
- operateRequestTask: (args) => abortTask(args, "requestTaskId"),
486
496
  createDownloadTask,
487
497
  createDownloadTaskAsync: createDownloadTask,
488
- operateDownloadTask: (args) => abortTask(args, "downloadTaskId"),
489
498
  createUploadTask,
490
499
  createUploadTaskAsync: createUploadTask,
500
+ createSocketTask
501
+ };
502
+ return {
503
+ ...Object.fromEntries(Object.entries(createTask).map(([api, handler]) => [api, rejectBaseLibInternal(handler)])),
504
+ operateRequestTask: (args) => abortTask(args, "requestTaskId"),
505
+ operateDownloadTask: (args) => abortTask(args, "downloadTaskId"),
491
506
  operateUploadTask: (args) => abortTask(args, "uploadTaskId"),
492
- createSocketTask,
493
507
  operateSocketTask
494
508
  };
495
509
  }
510
+ function rejectBaseLibInternal(handler) {
511
+ return (args, context) => args?.__skipDomainCheck__ === true ? fail(context.api, "\u57FA\u7840\u5E93\u5185\u90E8\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0Cwxa-runner \u4E0D\u53D1\u51FA") : handler(args, context);
512
+ }
496
513
  async function proxyFetch(url, init) {
497
514
  if (!/^https?:\/\//i.test(url)) throw new Error(`invalid url "${url}"`);
498
515
  const hasBody = init.body !== void 0 && init.body !== "" && !["GET", "HEAD"].includes(init.method);
@@ -1395,7 +1412,14 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1395
1412
  .wxa-toolbar button { border: 0; background: #111214; color: #fff; border-radius: 999px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
1396
1413
  .wxa-toolbar .wxa-scale { min-width: 52px; background: #fff; color: #333; box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); font-variant-numeric: tabular-nums; }
1397
1414
  .wxa-route { font-family: Menlo, monospace; }
1415
+ .wxa-notice { position: fixed; top: 16px; right: 16px; z-index: 100; display: flex; align-items: flex-start; gap: 10px; width: max-content; max-width: min(320px, calc(100vw - 32px)); padding: 12px 10px 12px 14px; box-sizing: border-box; border-radius: 12px; background: #fff; box-shadow: 0 0 0 1px rgba(15,23,42,.06), 0 12px 32px -8px rgba(15,23,42,.28); font-size: 13px; line-height: 20px; color: #555; animation: wxa-notice-in .18s ease-out; }
1416
+ .wxa-notice-icon { flex: none; width: 18px; height: 18px; margin-top: 1px; border-radius: 50%; background: #fa5151; color: #fff; font-size: 12px; font-weight: 700; line-height: 18px; text-align: center; }
1417
+ .wxa-notice-text { flex: 1; min-width: 0; word-break: break-word; text-wrap: pretty; }
1418
+ .wxa-notice-title { font-weight: 600; color: #111214; }
1419
+ .wxa-notice-close { flex: none; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 50%; background: none; color: #999; font-size: 16px; line-height: 20px; cursor: pointer; }
1420
+ .wxa-notice-close:hover { background: rgba(0,0,0,.06); color: #111214; }
1398
1421
  @keyframes wxa-spin { to { transform: rotate(360deg); } }
1422
+ @keyframes wxa-notice-in { from { opacity: 0; transform: translateY(-8px); } }
1399
1423
  `;
1400
1424
 
1401
1425
  // src/runtime/host/ui/Picker.ts
@@ -1484,6 +1508,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1484
1508
  // src/runtime/host/ui/PhoneShell.ts
1485
1509
  var SCALE_STORAGE_KEY = "wxa-runner:scale";
1486
1510
  var MIN_SCALE = 0.5;
1511
+ var NOTICE_DURATION = 6e3;
1487
1512
  function loadScaleMode() {
1488
1513
  try {
1489
1514
  const saved = localStorage.getItem(SCALE_STORAGE_KEY);
@@ -1498,6 +1523,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1498
1523
  this.scaleMode = loadScaleMode();
1499
1524
  this.toastEl = null;
1500
1525
  this.toastTimer = null;
1526
+ this.noticeEl = null;
1527
+ this.noticeTimer = null;
1501
1528
  this.tabBarConfig = null;
1502
1529
  this.onTabTap = () => void 0;
1503
1530
  this.navigation = { title: "", backgroundColor: "#000", textStyle: "white", showBack: false, custom: false };
@@ -1533,6 +1560,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1533
1560
  <span class="wxa-route"></span><button class="wxa-scale"></button><button class="wxa-reload">\u5237\u65B0</button>
1534
1561
  </div>
1535
1562
  </div>`;
1563
+ this.stage = root.querySelector(".wxa-stage");
1536
1564
  this.slot = root.querySelector(".wxa-device-slot");
1537
1565
  this.device = root.querySelector(".wxa-device");
1538
1566
  this.toolbar = root.querySelector(".wxa-toolbar");
@@ -1762,6 +1790,30 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1762
1790
  showImagePreview(urls, current) {
1763
1791
  openImagePreview(this.overlay, urls, current);
1764
1792
  }
1793
+ /**
1794
+ * 在手机外面提示 wxa-runner 自身的问题(如真实登录失败),不混进小程序的界面。
1795
+ * 同一时间只显示一条,新的替换旧的:业务代码反复重试时不会堆满屏幕
1796
+ */
1797
+ showNotice(title, message) {
1798
+ this.hideNotice();
1799
+ const notice = element("div", "wxa-notice");
1800
+ notice.setAttribute("role", "alert");
1801
+ const text = element("div", "wxa-notice-text");
1802
+ text.append(element("div", "wxa-notice-title", title), element("div", "wxa-notice-message", message));
1803
+ const close = element("button", "wxa-notice-close", "\xD7");
1804
+ close.setAttribute("aria-label", "\u5173\u95ED");
1805
+ close.addEventListener("click", () => this.hideNotice());
1806
+ notice.append(element("span", "wxa-notice-icon", "!"), text, close);
1807
+ this.stage.appendChild(notice);
1808
+ this.noticeEl = notice;
1809
+ this.noticeTimer = setTimeout(() => this.hideNotice(), NOTICE_DURATION);
1810
+ }
1811
+ hideNotice() {
1812
+ if (this.noticeTimer) clearTimeout(this.noticeTimer);
1813
+ this.noticeTimer = null;
1814
+ this.noticeEl?.remove();
1815
+ this.noticeEl = null;
1816
+ }
1765
1817
  showError(message) {
1766
1818
  this.clearError();
1767
1819
  const el = element("div", "wxa-error", message);
@@ -1799,6 +1851,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1799
1851
  /** setup 和测试 API 声明的请求拦截 */
1800
1852
  this.routes = new RequestRoutes();
1801
1853
  this.service = null;
1854
+ /** 新版基础库的业务子上下文;旧版基础库业务代码直接在逻辑层 iframe 里 */
1855
+ this.appContext = null;
1802
1856
  this.serviceReady = new Promise((resolve) => {
1803
1857
  this.serviceLoaded = resolve;
1804
1858
  });
@@ -1843,6 +1897,9 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1843
1897
  this.pages.get(webviewId)?.attach(win, bridge);
1844
1898
  }
1845
1899
  }
1900
+ registerAppContext(win) {
1901
+ this.appContext = win;
1902
+ }
1846
1903
  frameLoaded(kind, webviewId) {
1847
1904
  if (kind === "service") this.serviceLoaded();
1848
1905
  else this.pages.get(webviewId)?.loaded();
@@ -1851,7 +1908,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1851
1908
  const webviewId = kind === "webview" ? this.webviewIdOf(win) : this.currentPage()?.id ?? 0;
1852
1909
  const result = this.api.invoke(api, args, { api, from: kind, webviewId });
1853
1910
  const deliver = (res) => {
1854
- if (callback) callback(cloneInto(win, res));
1911
+ if (callback) callback(kind === "service" ? this.cloneForService(res) : cloneInto(win, res));
1855
1912
  };
1856
1913
  if (result && typeof result.then === "function") {
1857
1914
  result.then(deliver);
@@ -1873,7 +1930,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1873
1930
  const fromId = this.webviewIdOf(win);
1874
1931
  const service = this.service;
1875
1932
  if (!service) return;
1876
- const payload = cloneInto(service.win, data);
1933
+ const payload = this.cloneForService(data);
1877
1934
  queueMicrotask(() => service.bridge.__dispatchSub(event, payload, fromId));
1878
1935
  }
1879
1936
  reportError(kind, webviewId, error) {
@@ -1887,10 +1944,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1887
1944
  }
1888
1945
  /**
1889
1946
  * 业务代码只运行在逻辑层,所以只记逻辑层调用、名字在 wx 上的 API,每个 API 一次。
1890
- * 其余是基础库内部调用的原生接口(如 getAppContact、loadLibFilesSync),业务既没调用也 mock 不了
1947
+ * 其余是基础库内部调用的原生接口(如 getPermissionBytes、loadLibFilesSync),业务既没调用也 mock 不了
1891
1948
  */
1892
1949
  reportUnimplemented({ api, from }) {
1893
- const wx = this.service?.win?.wx;
1950
+ const wx = this.serviceWindow()?.wx;
1894
1951
  if (from !== "service" || typeof wx?.[api] !== "function") return;
1895
1952
  if (this.errors.some((error) => error.api === api)) return;
1896
1953
  this.errors.push({
@@ -1913,8 +1970,16 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1913
1970
  livePages() {
1914
1971
  return [...this.pages.values()];
1915
1972
  }
1973
+ /** 业务代码所在的 window:getCurrentPages / wx 等在它上面 */
1916
1974
  serviceWindow() {
1917
- return this.service?.win ?? null;
1975
+ return this.appContext ?? this.service?.win ?? null;
1976
+ }
1977
+ /**
1978
+ * 交给逻辑层的值属于业务代码所在的全局环境,而不是逻辑层 iframe:新版基础库不一定在业务子上下文里
1979
+ * 重建这些值,属于别的 iframe 的对象在业务代码里 instanceof 不成立,setData 也会把这样的数组当成普通对象
1980
+ */
1981
+ cloneForService(value) {
1982
+ return cloneInto(this.serviceWindow(), value);
1918
1983
  }
1919
1984
  onRoute(listener) {
1920
1985
  this.routeListeners.push(listener);
@@ -2073,7 +2138,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2073
2138
  emitToService(event, data, webviewId) {
2074
2139
  const service = this.service;
2075
2140
  if (!service) return;
2076
- const payload = cloneInto(service.win, data);
2141
+ const payload = this.cloneForService(data);
2077
2142
  queueMicrotask(() => service.bridge.__dispatchOn(event, payload, webviewId));
2078
2143
  }
2079
2144
  /** 派发给渲染层的原生事件(渲染层里 WeixinJSBridge.on 收到) */
@@ -2132,7 +2197,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2132
2197
  documentOf: (webviewId) => this.pages.get(webviewId)?.win?.document,
2133
2198
  emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data)
2134
2199
  }),
2135
- ...createMiscApis(this.info.appid, { realLogin: this.info.realLogin })
2200
+ ...createMiscApis(this.info.appid, {
2201
+ realLogin: this.info.realLogin,
2202
+ onRealLoginFail: (api, reason2) => this.shell.showNotice(`wx.${api} \u771F\u5B9E\u767B\u5F55\u5931\u8D25`, reason2)
2203
+ })
2136
2204
  });
2137
2205
  }
2138
2206
  };
@@ -2176,7 +2244,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2176
2244
  const sheets = win.__styleSheetManager2__?._pathMap;
2177
2245
  if (!sheets) throw new Error("\u57FA\u7840\u5E93\u6CA1\u6709\u6837\u5F0F\u8868\u7BA1\u7406\u5668\uFF0C\u65E0\u6CD5\u70ED\u66FF\u6362\u6837\u5F0F");
2178
2246
  const applied = Object.keys(sheets);
2179
- win.eval(code);
2247
+ runScript(win, code);
2180
2248
  for (const key of applied) {
2181
2249
  const [, file, classPrefix] = /^\.\/(.+\.wxss):(.*)$/.exec(key) || [];
2182
2250
  const inject = file && win.__wxAppCode__[file];
@@ -2196,7 +2264,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2196
2264
  function updateTemplates(win, code, units) {
2197
2265
  const cache = win.__WXML_GLOBAL__;
2198
2266
  if (cache) Object.assign(cache, { ops_cached: {}, ops_set: {}, ops_init: {}, debuginfo_set: {}, modules: {} });
2199
- win.eval(code);
2267
+ runScript(win, code);
2200
2268
  for (const id of units) {
2201
2269
  const generate = win.__wxAppCode__[id + ".wxml"];
2202
2270
  if (generate) win.__virtualDOM__.updateWxAppCode(id + ".wxml", generate);
@@ -2231,11 +2299,16 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2231
2299
  }
2232
2300
  function loadModule(service, script, hotId) {
2233
2301
  let factory;
2234
- service.eval(`(function (define) {
2235
- ${script}
2236
- })`)((_id, fn) => {
2302
+ service.__wxaHotDefine__ = (_id, fn) => {
2237
2303
  factory = fn;
2238
- });
2304
+ };
2305
+ try {
2306
+ runScript(service, `(function (define) {
2307
+ ${script}
2308
+ })(__wxaHotDefine__);`);
2309
+ } finally {
2310
+ delete service.__wxaHotDefine__;
2311
+ }
2239
2312
  if (typeof factory !== "function") throw new Error("\u6A21\u5757\u811A\u672C\u6CA1\u6709 define \u6A21\u5757");
2240
2313
  service.define(hotId, factory);
2241
2314
  const { Page, Component } = service;
@@ -2264,6 +2337,26 @@ ${script}
2264
2337
  const added = Object.keys(data).filter((key) => !(key in instance.data));
2265
2338
  if (added.length) instance.setData(Object.fromEntries(added.map((key) => [key, data[key]])));
2266
2339
  }
2340
+ function runScript(win, code) {
2341
+ let failed = false;
2342
+ let error;
2343
+ const onError = (event) => {
2344
+ failed = true;
2345
+ error = event.error ?? new Error(event.message);
2346
+ event.stopImmediatePropagation();
2347
+ event.preventDefault();
2348
+ };
2349
+ const script = win.document.createElement("script");
2350
+ script.text = code;
2351
+ win.addEventListener("error", onError, true);
2352
+ try {
2353
+ win.document.head.appendChild(script);
2354
+ } finally {
2355
+ win.removeEventListener("error", onError, true);
2356
+ script.remove();
2357
+ }
2358
+ if (failed) throw error;
2359
+ }
2267
2360
  async function fetchText(url) {
2268
2361
  const response = await fetch(url);
2269
2362
  if (!response.ok) throw new Error(`${url} ${response.status}`);