@open-wxa/wxa-runner-core 0.2.0 → 0.2.2

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.
@@ -85,17 +85,32 @@
85
85
  }
86
86
 
87
87
  // src/runtime/host/api/tempFiles.ts
88
+ var TEMP_DIR = "/__wxa__/tmp";
88
89
  async function saveTempFile(data, type) {
89
- const res = await fetch(`/__wxa__/tmp?type=${encodeURIComponent(type)}`, { method: "POST", body: data });
90
+ const res = await fetch(`${TEMP_DIR}?type=${encodeURIComponent(type)}`, { method: "POST", body: data });
90
91
  if (!res.ok) throw new Error(await res.text());
91
92
  const { url } = await res.json();
92
93
  return url;
93
94
  }
95
+ function tempFileRootPath(url) {
96
+ const prefix = `${location.origin}${TEMP_DIR}/`;
97
+ return url.startsWith(prefix) ? url.slice(location.origin.length) : url;
98
+ }
94
99
  function tempFileType(name, mimeType, fallback) {
95
100
  const fromName = /\.([a-z\d]+)$/i.exec(name)?.[1];
96
101
  const fromType = /^[a-z]+\/([a-z\d]+)\s*(;|$)/i.exec(mimeType)?.[1];
97
102
  return (fromName || fromType || fallback).toLowerCase();
98
103
  }
104
+ function isTempFile(filePath) {
105
+ return typeof filePath === "string" && filePath.startsWith(`${location.origin}${TEMP_DIR}/`);
106
+ }
107
+ async function tempFileSize(url) {
108
+ const res = await fetch(url, { method: "HEAD" });
109
+ return res.ok ? Number(res.headers.get("content-length")) : null;
110
+ }
111
+ async function removeTempFile(url) {
112
+ await fetch(url, { method: "DELETE" });
113
+ }
99
114
 
100
115
  // src/runtime/host/api/media.ts
101
116
  var SOURCES = ["camera", "album"];
@@ -236,6 +251,14 @@
236
251
  };
237
252
  function createMiscApis(appid, options) {
238
253
  const fs = new MemoryFileSystem();
254
+ const realLogin = async (api, url) => {
255
+ try {
256
+ return await postDevServer(url);
257
+ } catch (error) {
258
+ options?.onRealLoginFail?.(api, error.message);
259
+ throw error;
260
+ }
261
+ };
239
262
  const guard = (api, action) => {
240
263
  try {
241
264
  return ok(api, action() || {});
@@ -267,6 +290,9 @@
267
290
  unlink: (args, { api }) => guard(api, () => fs.remove(args.filePath, { dir: false })),
268
291
  rmdir: (args, { api }) => guard(api, () => fs.remove(args.dirPath, { dir: true, recursive: !!args.recursive })),
269
292
  getFileInfo: (args, { api }) => {
293
+ if (isTempFile(args.filePath)) {
294
+ return tempFileSize(args.filePath).then((size) => size === null ? fail(api, "file not exist") : ok(api, { size }));
295
+ }
270
296
  const node = fs.stat(args.filePath);
271
297
  return node ? ok(api, { size: node.data.length }) : fail(api, "file not exist");
272
298
  }
@@ -285,9 +311,9 @@
285
311
  operateWXData: (args, { api }) => args.data?.api_name === "webapi_getwxanetwork" ? bizDomainUnavailable(api) : ok(api),
286
312
  getAppContact: (args, { api }) => args.keyPath === "WxaAppInfo.Network.BizDomain" ? bizDomainUnavailable(api) : ok(api),
287
313
  // 真实登录失败时明确返回 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)}` }),
314
+ 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
315
  // 基础库只从 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" }),
316
+ 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
317
  checkSession: () => void 0,
292
318
  authorize: () => void 0,
293
319
  getSetting: (_args, { api }) => ok(api, {
@@ -823,6 +849,50 @@
823
849
  return text.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
824
850
  }
825
851
 
852
+ // src/runtime/host/api/savedFiles.ts
853
+ var NAMESPACE = "savedFiles";
854
+ function createSavedFileApis(storage) {
855
+ const record = (filePath) => storage.get(NAMESPACE, filePath)?.data;
856
+ const liveRecord = async (filePath) => {
857
+ const saved = record(filePath);
858
+ if (!saved) return void 0;
859
+ if (await tempFileSize(filePath) !== null) return saved;
860
+ storage.remove(NAMESPACE, filePath);
861
+ return void 0;
862
+ };
863
+ return {
864
+ // 和真机一样:存成新文件,原来的临时文件随之删除
865
+ saveFile: async (args) => {
866
+ if (args.filePath) throw new Error("wxa-runner \u6682\u4E0D\u652F\u6301\u6307\u5B9A filePath\uFF0C\u4E0D\u4F20\u65F6\u4FDD\u5B58\u6210\u529F\u540E\u8FD4\u56DE savedFilePath");
867
+ const res = isTempFile(args.tempFilePath) ? await fetch(args.tempFilePath) : null;
868
+ if (!res?.ok) throw new Error(`tempFilePath file not exist ${args.tempFilePath}`);
869
+ const data = await res.blob();
870
+ const savedFilePath = await saveTempFile(data, tempFileType(args.tempFilePath, data.type, "tmp"));
871
+ await removeTempFile(args.tempFilePath);
872
+ storage.set(NAMESPACE, savedFilePath, { size: data.size, createTime: Math.floor(Date.now() / 1e3) });
873
+ return { savedFilePath };
874
+ },
875
+ getSavedFileList: async () => {
876
+ const fileList = [];
877
+ for (const filePath of storage.keys(NAMESPACE)) {
878
+ const saved = await liveRecord(filePath);
879
+ if (saved) fileList.push({ filePath, ...saved });
880
+ }
881
+ return { fileList };
882
+ },
883
+ getSavedFileInfo: async (args) => {
884
+ const saved = await liveRecord(args.filePath);
885
+ if (!saved) throw new Error(`file not exist ${args.filePath}`);
886
+ return { ...saved };
887
+ },
888
+ removeSavedFile: async (args) => {
889
+ if (!record(args.filePath)) throw new Error(`file not exist ${args.filePath}`);
890
+ storage.remove(NAMESPACE, args.filePath);
891
+ await removeTempFile(args.filePath);
892
+ }
893
+ };
894
+ }
895
+
826
896
  // src/runtime/host/api/storage.ts
827
897
  var LIMIT_SIZE_KB = 10 * 1024;
828
898
  var MiniProgramStorage = class {
@@ -1404,7 +1474,14 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1404
1474
  .wxa-toolbar button { border: 0; background: #111214; color: #fff; border-radius: 999px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
1405
1475
  .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; }
1406
1476
  .wxa-route { font-family: Menlo, monospace; }
1477
+ .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; }
1478
+ .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; }
1479
+ .wxa-notice-text { flex: 1; min-width: 0; word-break: break-word; text-wrap: pretty; }
1480
+ .wxa-notice-title { font-weight: 600; color: #111214; }
1481
+ .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; }
1482
+ .wxa-notice-close:hover { background: rgba(0,0,0,.06); color: #111214; }
1407
1483
  @keyframes wxa-spin { to { transform: rotate(360deg); } }
1484
+ @keyframes wxa-notice-in { from { opacity: 0; transform: translateY(-8px); } }
1408
1485
  `;
1409
1486
 
1410
1487
  // src/runtime/host/ui/Picker.ts
@@ -1493,6 +1570,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1493
1570
  // src/runtime/host/ui/PhoneShell.ts
1494
1571
  var SCALE_STORAGE_KEY = "wxa-runner:scale";
1495
1572
  var MIN_SCALE = 0.5;
1573
+ var NOTICE_DURATION = 6e3;
1496
1574
  function loadScaleMode() {
1497
1575
  try {
1498
1576
  const saved = localStorage.getItem(SCALE_STORAGE_KEY);
@@ -1507,6 +1585,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1507
1585
  this.scaleMode = loadScaleMode();
1508
1586
  this.toastEl = null;
1509
1587
  this.toastTimer = null;
1588
+ this.noticeEl = null;
1589
+ this.noticeTimer = null;
1510
1590
  this.tabBarConfig = null;
1511
1591
  this.onTabTap = () => void 0;
1512
1592
  this.navigation = { title: "", backgroundColor: "#000", textStyle: "white", showBack: false, custom: false };
@@ -1542,6 +1622,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1542
1622
  <span class="wxa-route"></span><button class="wxa-scale"></button><button class="wxa-reload">\u5237\u65B0</button>
1543
1623
  </div>
1544
1624
  </div>`;
1625
+ this.stage = root.querySelector(".wxa-stage");
1545
1626
  this.slot = root.querySelector(".wxa-device-slot");
1546
1627
  this.device = root.querySelector(".wxa-device");
1547
1628
  this.toolbar = root.querySelector(".wxa-toolbar");
@@ -1771,6 +1852,30 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1771
1852
  showImagePreview(urls, current) {
1772
1853
  openImagePreview(this.overlay, urls, current);
1773
1854
  }
1855
+ /**
1856
+ * 在手机外面提示 wxa-runner 自身的问题(如真实登录失败),不混进小程序的界面。
1857
+ * 同一时间只显示一条,新的替换旧的:业务代码反复重试时不会堆满屏幕
1858
+ */
1859
+ showNotice(title, message) {
1860
+ this.hideNotice();
1861
+ const notice = element("div", "wxa-notice");
1862
+ notice.setAttribute("role", "alert");
1863
+ const text = element("div", "wxa-notice-text");
1864
+ text.append(element("div", "wxa-notice-title", title), element("div", "wxa-notice-message", message));
1865
+ const close = element("button", "wxa-notice-close", "\xD7");
1866
+ close.setAttribute("aria-label", "\u5173\u95ED");
1867
+ close.addEventListener("click", () => this.hideNotice());
1868
+ notice.append(element("span", "wxa-notice-icon", "!"), text, close);
1869
+ this.stage.appendChild(notice);
1870
+ this.noticeEl = notice;
1871
+ this.noticeTimer = setTimeout(() => this.hideNotice(), NOTICE_DURATION);
1872
+ }
1873
+ hideNotice() {
1874
+ if (this.noticeTimer) clearTimeout(this.noticeTimer);
1875
+ this.noticeTimer = null;
1876
+ this.noticeEl?.remove();
1877
+ this.noticeEl = null;
1878
+ }
1774
1879
  showError(message) {
1775
1880
  this.clearError();
1776
1881
  const el = element("div", "wxa-error", message);
@@ -1876,9 +1981,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
1876
1981
  publish(kind, win, event, data, webviewIds) {
1877
1982
  if (kind === "service") {
1878
1983
  const targets = webviewIds?.length ? webviewIds.map((id) => this.pages.get(Number(id))) : this.livePages();
1984
+ const message = event === "canvasActionsChanged" ? withDrawableImages(data) : data;
1879
1985
  for (const page of targets) {
1880
1986
  page?.dispatch((bridge, target) => {
1881
- const payload2 = cloneInto(target, data);
1987
+ const payload2 = cloneInto(target, message);
1882
1988
  queueMicrotask(() => bridge.__dispatchSub(event, payload2, 0));
1883
1989
  });
1884
1990
  }
@@ -2126,6 +2232,7 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2126
2232
  reLaunch: routeApi((args) => this.reLaunch(args.url)),
2127
2233
  switchTab: routeApi((args) => this.switchTab(args.url)),
2128
2234
  ...createStorageApis(this.storage),
2235
+ ...createSavedFileApis(this.storage),
2129
2236
  ...createSystemApis({
2130
2237
  device: this.device,
2131
2238
  baseLibVersion: this.info.baseLibVersion,
@@ -2154,7 +2261,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2154
2261
  documentOf: (webviewId) => this.pages.get(webviewId)?.win?.document,
2155
2262
  emitToWebview: (webviewId, event, data) => this.emitToWebview(webviewId, event, data)
2156
2263
  }),
2157
- ...createMiscApis(this.info.appid, { realLogin: this.info.realLogin })
2264
+ ...createMiscApis(this.info.appid, {
2265
+ realLogin: this.info.realLogin,
2266
+ onRealLoginFail: (api, reason2) => this.shell.showNotice(`wx.${api} \u771F\u5B9E\u767B\u5F55\u5931\u8D25`, reason2)
2267
+ })
2158
2268
  });
2159
2269
  }
2160
2270
  };
@@ -2162,6 +2272,20 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
2162
2272
  if (value === void 0) return value;
2163
2273
  return win.JSON.parse(JSON.stringify(value));
2164
2274
  }
2275
+ function withDrawableImages(message) {
2276
+ const actions = message?.data?.actions;
2277
+ if (!Array.isArray(actions)) return message;
2278
+ return {
2279
+ ...message,
2280
+ data: {
2281
+ ...message.data,
2282
+ actions: actions.map((action) => {
2283
+ const [src, ...rest] = action.data ?? [];
2284
+ return action.method === "drawImage" && typeof src === "string" ? { ...action, data: [tempFileRootPath(src), ...rest] } : action;
2285
+ })
2286
+ }
2287
+ };
2288
+ }
2165
2289
  function stripHtml(path) {
2166
2290
  return path.replace(/\.html$/, "");
2167
2291
  }