@open-wxa/wxa-runner-core 0.7.0 → 0.8.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.
- package/dist/index.d.mts +27 -10
- package/dist/index.d.ts +27 -10
- package/dist/index.js +380 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +376 -55
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/frame.js +44 -2
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/host.js +443 -175
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/sw.js +56 -0
- package/dist/runtime/sw.js.map +1 -0
- package/dist/{setup-DZ5FeKay.d.mts → setup-Bac8XdhR.d.mts} +5 -1
- package/dist/{setup-DZ5FeKay.d.ts → setup-Bac8XdhR.d.ts} +5 -1
- package/dist/setup.d.mts +1 -1
- package/dist/setup.d.ts +1 -1
- package/dist/test-api.d.mts +2 -2
- package/dist/test-api.d.ts +2 -2
- package/dist/{testApi-BLozOgCt.d.ts → testApi-BOZSXSnV.d.ts} +3 -3
- package/dist/{testApi-CnZOLyWw.d.mts → testApi-DHuEW5Cl.d.mts} +3 -3
- package/package.json +4 -4
package/dist/runtime/host.js
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// src/runtime/host/ConsoleForwarder.ts
|
|
4
|
+
var DEDUPE_MS = 1e3;
|
|
5
|
+
var MAX_ARG_LENGTH = 1e3;
|
|
6
|
+
var MAX_TEXT_LENGTH = 8e3;
|
|
7
|
+
var LAYERS = { service: "\u903B\u8F91\u5C42", webview: "\u6E32\u67D3\u5C42" };
|
|
8
|
+
var MINI_PROGRAM_ERROR = /^(\(in promise\) )?MiniProgramError\n/;
|
|
9
|
+
var MODULE_URL = /\bhttps?:\/\/[^/\s]+\/__wxa__\/module\/([^?\s)]+?)(?:\?[^:\s)]*)?:(\d+):(\d+)/;
|
|
10
|
+
var ConsoleForwarder = class {
|
|
11
|
+
/** @param pageOf 出错的页面路径,不知道时返回 undefined */
|
|
12
|
+
constructor(mode, send, pageOf) {
|
|
13
|
+
this.mode = mode;
|
|
14
|
+
this.send = send;
|
|
15
|
+
this.pageOf = pageOf;
|
|
16
|
+
this.recentErrors = /* @__PURE__ */ new Map();
|
|
17
|
+
}
|
|
18
|
+
/** 运行时错误:全局 error、未处理的 Promise 拒绝、基础库捕获的业务代码错误 */
|
|
19
|
+
error(kind, webviewId, message, stack) {
|
|
20
|
+
const frames = businessFrames(stack);
|
|
21
|
+
if (!this.mode || this.isRepeated(`${kind}:${webviewId}:${message}
|
|
22
|
+
${frames}`)) return;
|
|
23
|
+
this.post("error", `${this.origin(kind, webviewId)}\u62A5\u9519\uFF1A${message}${frames ? `
|
|
24
|
+
${frames}` : ""}`);
|
|
25
|
+
}
|
|
26
|
+
/** 运行时自己的提醒,如调用了未实现的 wx API */
|
|
27
|
+
warn(text) {
|
|
28
|
+
if (this.mode) this.post("warn", text);
|
|
29
|
+
}
|
|
30
|
+
/** iframe 里的 console.warn / console.error;基础库捕获业务代码错误后打印的 console.error 按报错转发 */
|
|
31
|
+
console(kind, webviewId, level, args) {
|
|
32
|
+
const thrown = level === "error" ? miniProgramError(args[0]) : null;
|
|
33
|
+
if (thrown) {
|
|
34
|
+
this.error(kind, webviewId, thrown.message, thrown.stack);
|
|
35
|
+
} else if (this.mode === "all") {
|
|
36
|
+
this.post(level, `${this.origin(kind, webviewId)} console.${level}\uFF1A${args.map(formatArg).join(" ")}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
post(level, text) {
|
|
40
|
+
this.send({ type: "console", level, text: text.length > MAX_TEXT_LENGTH ? `${text.slice(0, MAX_TEXT_LENGTH)}\u2026` : text });
|
|
41
|
+
}
|
|
42
|
+
origin(kind, webviewId) {
|
|
43
|
+
const page = this.pageOf(kind, webviewId);
|
|
44
|
+
return page ? `${LAYERS[kind]}\uFF08${page}\uFF09` : LAYERS[kind];
|
|
45
|
+
}
|
|
46
|
+
/** @param key 来源、错误信息和业务代码的帧:两路报来的同一个错误相同,不同位置抛出的同名错误不同 */
|
|
47
|
+
isRepeated(key) {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
for (const [recent, time] of this.recentErrors) {
|
|
50
|
+
if (now - time >= DEDUPE_MS) this.recentErrors.delete(recent);
|
|
51
|
+
}
|
|
52
|
+
if (this.recentErrors.has(key)) return true;
|
|
53
|
+
this.recentErrors.set(key, now);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function isErrorLike(value) {
|
|
58
|
+
return typeof value === "object" && value !== null && typeof value.message === "string";
|
|
59
|
+
}
|
|
60
|
+
function miniProgramError(value) {
|
|
61
|
+
if (isErrorLike(value) && MINI_PROGRAM_ERROR.test(value.message)) {
|
|
62
|
+
return { message: value.message.replace(MINI_PROGRAM_ERROR, ""), stack: value.stack };
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "string" && MINI_PROGRAM_ERROR.test(value)) {
|
|
65
|
+
const text = value.replace(MINI_PROGRAM_ERROR, "");
|
|
66
|
+
const firstFrame2 = text.search(/^\s*at /m);
|
|
67
|
+
const head = (firstFrame2 < 0 ? text : text.slice(0, firstFrame2)).replace(/\n$/, "");
|
|
68
|
+
const message = /^([\s\S]*)\n[^\n]*?: \1$/.exec(head)?.[1] ?? head;
|
|
69
|
+
return { message, stack: firstFrame2 < 0 ? void 0 : text.slice(firstFrame2) };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function businessFrames(stack) {
|
|
74
|
+
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");
|
|
75
|
+
}
|
|
76
|
+
function formatArg(arg) {
|
|
77
|
+
if (typeof arg === "string") return truncate(arg);
|
|
78
|
+
if (isErrorLike(arg)) {
|
|
79
|
+
const frames = businessFrames(arg.stack);
|
|
80
|
+
return truncate(`${arg.name || "Error"}: ${arg.message}`) + (frames ? `
|
|
81
|
+
${frames}` : "");
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const json = JSON.stringify(arg);
|
|
85
|
+
if (json !== void 0) return truncate(json);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return truncate(String(arg));
|
|
89
|
+
}
|
|
90
|
+
function truncate(text) {
|
|
91
|
+
return text.length > MAX_ARG_LENGTH ? `${text.slice(0, MAX_ARG_LENGTH)}\u2026` : text;
|
|
92
|
+
}
|
|
93
|
+
|
|
3
94
|
// src/runtime/host/api/registry.ts
|
|
4
95
|
var SILENT_APIS = /* @__PURE__ */ new Set([
|
|
5
96
|
"reportKeyValue",
|
|
@@ -205,154 +296,103 @@
|
|
|
205
296
|
};
|
|
206
297
|
}
|
|
207
298
|
|
|
208
|
-
// src/runtime/
|
|
209
|
-
var
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const prefix = `${location.origin}${TEMP_DIR}/`;
|
|
218
|
-
return url.startsWith(prefix) ? url.slice(location.origin.length) : url;
|
|
219
|
-
}
|
|
220
|
-
function tempFileType(name, mimeType, fallback) {
|
|
221
|
-
const fromName = /\.([a-z\d]+)$/i.exec(name)?.[1];
|
|
222
|
-
const fromType = /^[a-z]+\/([a-z\d]+)\s*(;|$)/i.exec(mimeType)?.[1];
|
|
223
|
-
return (fromName || fromType || fallback).toLowerCase();
|
|
224
|
-
}
|
|
225
|
-
function isTempFile(filePath) {
|
|
226
|
-
return typeof filePath === "string" && filePath.startsWith(`${location.origin}${TEMP_DIR}/`);
|
|
227
|
-
}
|
|
228
|
-
async function tempFileSize(url) {
|
|
229
|
-
const res = await fetch(url, { method: "HEAD" });
|
|
230
|
-
return res.ok ? Number(res.headers.get("content-length")) : null;
|
|
231
|
-
}
|
|
232
|
-
async function removeTempFile(url) {
|
|
233
|
-
await fetch(url, { method: "DELETE" });
|
|
299
|
+
// src/runtime/fileSession.ts
|
|
300
|
+
var KEY = "__wxa_file_session__";
|
|
301
|
+
function fileSession() {
|
|
302
|
+
let session = localStorage.getItem(KEY);
|
|
303
|
+
if (!session) {
|
|
304
|
+
session = crypto.randomUUID();
|
|
305
|
+
localStorage.setItem(KEY, session);
|
|
306
|
+
}
|
|
307
|
+
return session;
|
|
234
308
|
}
|
|
235
309
|
|
|
236
310
|
// src/runtime/host/api/fs.ts
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
function createFileSystemApis() {
|
|
300
|
-
const fs = new MemoryFileSystem();
|
|
301
|
-
const guard = (api, action) => {
|
|
302
|
-
try {
|
|
303
|
-
return ok(api, action() || {});
|
|
304
|
-
} catch (error) {
|
|
305
|
-
return fail(api, error.message);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
const statResult = (node) => {
|
|
309
|
-
const stats = {
|
|
310
|
-
mode: node.dir ? DIR_MODE : FILE_MODE,
|
|
311
|
-
size: node.data.length,
|
|
312
|
-
lastAccessedTime: Math.floor(node.mtime / 1e3),
|
|
313
|
-
lastModifiedTime: Math.floor(node.mtime / 1e3)
|
|
314
|
-
};
|
|
315
|
-
return { stats, ...stats };
|
|
316
|
-
};
|
|
317
|
-
const readMemoryFile = (args, { api }) => guard(api, () => ({ data: fs.read(args.filePath) }));
|
|
318
|
-
const apis = {
|
|
319
|
-
access: (args, { api }) => fs.stat(args.path) ? ok(api) : fail(api, `no such file or directory ${args.path}`),
|
|
320
|
-
stat: (args, { api }) => {
|
|
321
|
-
const node = fs.stat(args.path);
|
|
322
|
-
return node ? ok(api, statResult(node)) : fail(api, `no such file or directory ${args.path}`);
|
|
323
|
-
},
|
|
324
|
-
mkdir: (args, { api }) => guard(api, () => fs.mkdir(args.dirPath, !!args.recursive)),
|
|
325
|
-
writeFile: (args, { api }) => guard(api, () => fs.write(args.filePath, String(args.data ?? ""))),
|
|
326
|
-
appendFile: (args, { api }) => guard(api, () => fs.write(args.filePath, String(args.data ?? ""), true)),
|
|
327
|
-
readdir: (args, { api }) => guard(api, () => ({ files: fs.readdir(args.dirPath) })),
|
|
328
|
-
unlink: (args, { api }) => guard(api, () => fs.remove(args.filePath, { dir: false })),
|
|
329
|
-
rmdir: (args, { api }) => guard(api, () => fs.remove(args.dirPath, { dir: true, recursive: !!args.recursive })),
|
|
330
|
-
getFileInfo: (args, { api }) => {
|
|
331
|
-
if (isTempFile(args.filePath)) {
|
|
332
|
-
return tempFileSize(args.filePath).then((size) => size === null ? fail(api, "file not exist") : ok(api, { size }));
|
|
311
|
+
function requestFile(operation, args, sync) {
|
|
312
|
+
const url = "/__wxa__/fs/" + fileSession();
|
|
313
|
+
const body = JSON.stringify({ operation, args });
|
|
314
|
+
if (!sync)
|
|
315
|
+
return fetch(url, {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "content-type": "application/json" },
|
|
318
|
+
body
|
|
319
|
+
}).then(async (response) => fileResult(response.status, await response.text()));
|
|
320
|
+
const request = new XMLHttpRequest();
|
|
321
|
+
request.open("POST", url, false);
|
|
322
|
+
request.setRequestHeader("content-type", "application/json");
|
|
323
|
+
request.send(body);
|
|
324
|
+
return fileResult(request.status, request.responseText);
|
|
325
|
+
}
|
|
326
|
+
function fileResult(status, text) {
|
|
327
|
+
if (status !== 200) throw new Error(`filesystem HTTP ${status}`);
|
|
328
|
+
const { result, error } = JSON.parse(text);
|
|
329
|
+
if (error) throw new Error(error);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
function createFileSystemApis(request = requestFile) {
|
|
333
|
+
const apis = {};
|
|
334
|
+
for (const operation of [
|
|
335
|
+
"access",
|
|
336
|
+
"stat",
|
|
337
|
+
"mkdir",
|
|
338
|
+
"writeFile",
|
|
339
|
+
"appendFile",
|
|
340
|
+
"readdir",
|
|
341
|
+
"unlink",
|
|
342
|
+
"rmdir",
|
|
343
|
+
"getFileInfo",
|
|
344
|
+
"copyFile",
|
|
345
|
+
"readFile"
|
|
346
|
+
]) {
|
|
347
|
+
const handler = (args, { api }) => {
|
|
348
|
+
try {
|
|
349
|
+
const input = { ...args };
|
|
350
|
+
const isBuffer = Object.prototype.toString.call(input.data) === "[object ArrayBuffer]";
|
|
351
|
+
if (isBuffer || ArrayBuffer.isView(input.data)) {
|
|
352
|
+
const view = isBuffer ? new Uint8Array(input.data) : new Uint8Array(input.data.buffer, input.data.byteOffset, input.data.byteLength);
|
|
353
|
+
let binary = "";
|
|
354
|
+
for (let offset = 0; offset < view.length; offset += 32768) {
|
|
355
|
+
binary += String.fromCharCode(...view.subarray(offset, offset + 32768));
|
|
356
|
+
}
|
|
357
|
+
input.data = btoa(binary);
|
|
358
|
+
input.binary = true;
|
|
359
|
+
}
|
|
360
|
+
const decode = (result2) => {
|
|
361
|
+
if (result2.binary) {
|
|
362
|
+
result2.data = Uint8Array.from(atob(result2.data), (character) => character.charCodeAt(0)).buffer;
|
|
363
|
+
delete result2.binary;
|
|
364
|
+
}
|
|
365
|
+
return ok(api, result2);
|
|
366
|
+
};
|
|
367
|
+
const result = request(operation, input, api.endsWith("Sync"));
|
|
368
|
+
return result instanceof Promise ? result.then(decode, (error) => fail(api, error.message)) : decode(result);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return fail(api, error.message);
|
|
333
371
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
),
|
|
348
|
-
readFileSync: readMemoryFile
|
|
349
|
-
};
|
|
372
|
+
};
|
|
373
|
+
apis[operation] = handler;
|
|
374
|
+
apis[operation + "Sync"] = handler;
|
|
375
|
+
apis["fs_" + operation] = handler;
|
|
376
|
+
apis["fs_" + operation + "Sync"] = handler;
|
|
377
|
+
}
|
|
378
|
+
const readUserFile = apis.readFile;
|
|
379
|
+
apis.readFile = (args, context) => /^[a-z]+:\/\//i.test(String(args.filePath)) || /^\/__wxa__\/(tmp|usr)\//.test(String(args.filePath)) ? readUserFile(args, context) : readPackageFile(args.filePath, args.encoding).then(
|
|
380
|
+
(data) => ok(context.api, { data }),
|
|
381
|
+
(error) => fail(context.api, error.message)
|
|
382
|
+
);
|
|
383
|
+
apis.fs_readFile = apis.readFile;
|
|
384
|
+
return apis;
|
|
350
385
|
}
|
|
351
386
|
async function readPackageFile(filePath, encoding) {
|
|
352
387
|
if (encoding !== void 0 && encoding !== "utf8" && encoding !== "utf-8") {
|
|
353
388
|
throw new Error(`\u8BFB\u53D6\u4EE3\u7801\u5305\u91CC\u7684\u6587\u4EF6\u53EA\u652F\u6301 utf8 \u7F16\u7801\uFF0C\u6216\u4E0D\u6307\u5B9A\u7F16\u7801\u5F97\u5230 ArrayBuffer\uFF0C\u4E0D\u652F\u6301 ${encoding}`);
|
|
354
389
|
}
|
|
355
|
-
const
|
|
390
|
+
const parts = [];
|
|
391
|
+
for (const part of String(filePath).split("/")) {
|
|
392
|
+
if (part === "..") parts.pop();
|
|
393
|
+
else if (part && part !== ".") parts.push(part);
|
|
394
|
+
}
|
|
395
|
+
const response = await fetch("/" + parts.map(encodeURIComponent).join("/"));
|
|
356
396
|
if (!response.ok) throw new Error(`no such file or directory, open ${filePath}`);
|
|
357
397
|
return encoding ? response.text() : response.arrayBuffer();
|
|
358
398
|
}
|
|
@@ -429,6 +469,34 @@
|
|
|
429
469
|
}
|
|
430
470
|
};
|
|
431
471
|
|
|
472
|
+
// src/runtime/host/api/tempFiles.ts
|
|
473
|
+
var TEMP_DIR = "/__wxa__/tmp";
|
|
474
|
+
async function saveTempFile(data, type) {
|
|
475
|
+
const res = await fetch(`${TEMP_DIR}/${fileSession()}?type=${encodeURIComponent(type)}`, { method: "POST", body: data });
|
|
476
|
+
if (!res.ok) throw new Error(await res.text());
|
|
477
|
+
const { url } = await res.json();
|
|
478
|
+
return url;
|
|
479
|
+
}
|
|
480
|
+
function tempFileRootPath(url) {
|
|
481
|
+
const prefix = `${location.origin}${TEMP_DIR}/`;
|
|
482
|
+
return url.startsWith(prefix) ? url.slice(location.origin.length) : url;
|
|
483
|
+
}
|
|
484
|
+
function tempFileType(name, mimeType, fallback) {
|
|
485
|
+
const fromName = /\.([a-z\d]+)$/i.exec(name)?.[1];
|
|
486
|
+
const fromType = /^[a-z]+\/([a-z\d]+)\s*(;|$)/i.exec(mimeType)?.[1];
|
|
487
|
+
return (fromName || fromType || fallback).toLowerCase();
|
|
488
|
+
}
|
|
489
|
+
function isTempFile(filePath) {
|
|
490
|
+
return typeof filePath === "string" && filePath.startsWith(`${location.origin}${TEMP_DIR}/`);
|
|
491
|
+
}
|
|
492
|
+
async function tempFileSize(url) {
|
|
493
|
+
const res = await fetch(url, { method: "HEAD" });
|
|
494
|
+
return res.ok ? Number(res.headers.get("content-length")) : null;
|
|
495
|
+
}
|
|
496
|
+
async function removeTempFile(url) {
|
|
497
|
+
await fetch(url, { method: "DELETE" });
|
|
498
|
+
}
|
|
499
|
+
|
|
432
500
|
// src/runtime/host/api/media.ts
|
|
433
501
|
var SOURCES = ["camera", "album"];
|
|
434
502
|
var SOURCE_LABELS = { camera: "\u62CD\u6444", album: "\u4ECE\u624B\u673A\u76F8\u518C\u9009\u62E9" };
|
|
@@ -630,6 +698,32 @@
|
|
|
630
698
|
return fail(api, "not available in wxa-runner");
|
|
631
699
|
}
|
|
632
700
|
|
|
701
|
+
// src/runtime/networkPanel.ts
|
|
702
|
+
var PROXY_PREFIX = "/__wxa__/proxy/";
|
|
703
|
+
var MOCK_PREFIX = "/__wxa__/mock/";
|
|
704
|
+
var MOCK_ID_HEADER = "x-wxa-mock-id";
|
|
705
|
+
function toServerPath(prefix, url) {
|
|
706
|
+
let target;
|
|
707
|
+
try {
|
|
708
|
+
target = new URL(url);
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") return null;
|
|
713
|
+
return `${prefix}${target.protocol.slice(0, -1)}/${target.host}${target.pathname}${target.search}`;
|
|
714
|
+
}
|
|
715
|
+
function toHeaders(header) {
|
|
716
|
+
const headers = new Headers();
|
|
717
|
+
for (const [name, value] of Object.entries(header)) {
|
|
718
|
+
if (value === void 0 || value === null) continue;
|
|
719
|
+
try {
|
|
720
|
+
headers.set(name, String(value));
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return headers;
|
|
725
|
+
}
|
|
726
|
+
|
|
633
727
|
// src/runtime/host/api/network.ts
|
|
634
728
|
var RequestLog = class {
|
|
635
729
|
constructor(limit = 1e3) {
|
|
@@ -676,8 +770,9 @@
|
|
|
676
770
|
options.onRequest?.({ ...request, header: { ...request.header }, routedBy: route ? route.pattern : null });
|
|
677
771
|
const requestTaskId = startTask(args, async (signal, id) => {
|
|
678
772
|
try {
|
|
679
|
-
const mocked = route
|
|
680
|
-
|
|
773
|
+
const mocked = route?.respond(signal);
|
|
774
|
+
if (mocked) options.onMocked?.(request, mocked, signal);
|
|
775
|
+
const res = mocked ? await mocked.then(({ statusCode, header, body }) => ({ statusCode, header, cookies: [], response: new Response(body) })) : await proxyFetch(request.url, { method: request.method, header: args.header, body: args.data, signal });
|
|
681
776
|
emit("onRequestTaskStateChange", {
|
|
682
777
|
requestTaskId: id,
|
|
683
778
|
state: "headersReceived",
|
|
@@ -685,7 +780,36 @@
|
|
|
685
780
|
statusCode: res.statusCode,
|
|
686
781
|
cookies: res.cookies
|
|
687
782
|
});
|
|
688
|
-
|
|
783
|
+
let data;
|
|
784
|
+
if (args.enableChunked && res.response.body) {
|
|
785
|
+
const reader = res.response.body.getReader();
|
|
786
|
+
const chunks = [];
|
|
787
|
+
let length = 0;
|
|
788
|
+
try {
|
|
789
|
+
for (; ; ) {
|
|
790
|
+
const { done, value } = await reader.read();
|
|
791
|
+
if (done) break;
|
|
792
|
+
chunks.push(value);
|
|
793
|
+
length += value.byteLength;
|
|
794
|
+
emit("onRequestTaskStateChange", {
|
|
795
|
+
requestTaskId: id,
|
|
796
|
+
state: "chunked",
|
|
797
|
+
data: value.slice().buffer
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
} finally {
|
|
801
|
+
reader.releaseLock();
|
|
802
|
+
}
|
|
803
|
+
const bytes = new Uint8Array(length);
|
|
804
|
+
let offset = 0;
|
|
805
|
+
for (const chunk of chunks) {
|
|
806
|
+
bytes.set(chunk, offset);
|
|
807
|
+
offset += chunk.byteLength;
|
|
808
|
+
}
|
|
809
|
+
data = args.responseType === "arraybuffer" ? bytes.buffer : new TextDecoder().decode(bytes);
|
|
810
|
+
} else {
|
|
811
|
+
data = args.responseType === "arraybuffer" ? await res.response.arrayBuffer() : await res.response.text();
|
|
812
|
+
}
|
|
689
813
|
emit("onRequestTaskStateChange", {
|
|
690
814
|
requestTaskId: id,
|
|
691
815
|
state: "success",
|
|
@@ -733,7 +857,9 @@
|
|
|
733
857
|
const createUploadTask = (args, { api }) => {
|
|
734
858
|
const uploadTaskId = startTask(args, async (signal, id) => {
|
|
735
859
|
try {
|
|
736
|
-
const
|
|
860
|
+
const source = await fetch(args.filePath);
|
|
861
|
+
if (!source.ok) throw new Error(`file not exist ${args.filePath}`);
|
|
862
|
+
const file = await source.blob();
|
|
737
863
|
const form = new FormData();
|
|
738
864
|
for (const [key, value] of Object.entries(args.formData || {})) form.append(key, String(value));
|
|
739
865
|
form.append(args.name || "file", file, String(args.filePath).split("/").pop() || "file");
|
|
@@ -824,12 +950,12 @@
|
|
|
824
950
|
return args.responseType === "arraybuffer" && args.enableCache === true;
|
|
825
951
|
}
|
|
826
952
|
async function proxyFetch(url, init) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const response = await fetch(
|
|
953
|
+
const path = toServerPath(PROXY_PREFIX, url);
|
|
954
|
+
if (!path) throw new Error(`invalid url "${url}"`);
|
|
955
|
+
const response = await fetch(path, {
|
|
830
956
|
method: init.method,
|
|
831
957
|
headers: { "x-wxa-headers": JSON.stringify(init.header || {}) },
|
|
832
|
-
body:
|
|
958
|
+
body: hasRequestBody(init.method, init.body) ? init.body : void 0,
|
|
833
959
|
signal: init.signal
|
|
834
960
|
});
|
|
835
961
|
if (response.headers.get("x-wxa-proxy-error")) {
|
|
@@ -842,11 +968,56 @@
|
|
|
842
968
|
response
|
|
843
969
|
};
|
|
844
970
|
}
|
|
971
|
+
function hasRequestBody(method, body) {
|
|
972
|
+
return body !== void 0 && body !== "" && !["GET", "HEAD"].includes(method);
|
|
973
|
+
}
|
|
845
974
|
function reason(error) {
|
|
846
975
|
if (error instanceof DOMException && error.name === "AbortError") return "abort";
|
|
847
976
|
return error instanceof Error ? error.message : String(error);
|
|
848
977
|
}
|
|
849
978
|
|
|
979
|
+
// src/runtime/host/api/mockMirror.ts
|
|
980
|
+
var SERVICE_WORKER_URL = "/__wxa__/runtime/sw.js";
|
|
981
|
+
var MockMirror = class {
|
|
982
|
+
constructor(serviceWorker) {
|
|
983
|
+
this.serviceWorker = serviceWorker;
|
|
984
|
+
/** 正在显示的请求,Service Worker 按 id 来要响应 */
|
|
985
|
+
this.replies = /* @__PURE__ */ new Map();
|
|
986
|
+
this.seq = 0;
|
|
987
|
+
serviceWorker.addEventListener("message", (event) => this.answer(event));
|
|
988
|
+
serviceWorker.startMessages();
|
|
989
|
+
}
|
|
990
|
+
/** response 失败(规则出错、请求被取消)时这一行显示为失败;signal abort 时显示为取消 */
|
|
991
|
+
show(request, response, signal) {
|
|
992
|
+
const path = toServerPath(MOCK_PREFIX, request.url);
|
|
993
|
+
if (!path || !this.serviceWorker.controller) return;
|
|
994
|
+
const id = String(++this.seq);
|
|
995
|
+
const cancelled = new Promise((resolve) => signal.addEventListener("abort", () => resolve(null), { once: true }));
|
|
996
|
+
this.replies.set(id, Promise.race([response.catch(() => null), cancelled]));
|
|
997
|
+
const headers = toHeaders(request.header);
|
|
998
|
+
headers.set(MOCK_ID_HEADER, id);
|
|
999
|
+
fetch(path, {
|
|
1000
|
+
method: request.method,
|
|
1001
|
+
headers,
|
|
1002
|
+
body: hasRequestBody(request.method, request.data) ? request.data : void 0,
|
|
1003
|
+
// 拦截规则给出 3xx 时只显示这个响应:跟随 Location 会真的发出请求,小程序那边也不跟随
|
|
1004
|
+
redirect: "manual",
|
|
1005
|
+
signal
|
|
1006
|
+
}).catch(() => void 0).finally(() => this.replies.delete(id));
|
|
1007
|
+
}
|
|
1008
|
+
answer(event) {
|
|
1009
|
+
const query = event.data;
|
|
1010
|
+
const port = event.ports[0];
|
|
1011
|
+
if (query?.type !== "wxa-mock-response" || !port) return;
|
|
1012
|
+
(this.replies.get(query.id) ?? Promise.resolve(null)).then((reply) => port.postMessage(reply));
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
function installMockMirror() {
|
|
1016
|
+
if (navigator.webdriver || !navigator.serviceWorker) return null;
|
|
1017
|
+
navigator.serviceWorker.register(SERVICE_WORKER_URL, { scope: "/" }).catch((error) => console.warn("[wxa-runner] Service Worker \u6CE8\u518C\u5931\u8D25\uFF0C\u88AB\u62E6\u622A\u7684\u8BF7\u6C42\u4E0D\u4F1A\u663E\u793A\u5728 Network \u9762\u677F\u91CC", error));
|
|
1018
|
+
return new MockMirror(navigator.serviceWorker);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
850
1021
|
// src/runtime/host/api/picker.ts
|
|
851
1022
|
async function fetchRegionData(kind) {
|
|
852
1023
|
const res = await fetch(`/__wxa__/region/${kind}`);
|
|
@@ -1471,7 +1642,8 @@
|
|
|
1471
1642
|
}
|
|
1472
1643
|
|
|
1473
1644
|
// src/runtime/host/api/ui.ts
|
|
1474
|
-
function createUiApis({ shell, updateNavigation }) {
|
|
1645
|
+
function createUiApis({ shell, updateNavigation, setTabBarHidden }) {
|
|
1646
|
+
const updateTabBarItem = (api, index, item) => shell.updateTabBarItem(Number(index), item) ? void 0 : fail(api, "tabbar item not found");
|
|
1475
1647
|
return {
|
|
1476
1648
|
showToast: (args) => shell.showToast(args),
|
|
1477
1649
|
hideToast: () => shell.hideToast(),
|
|
@@ -1489,6 +1661,22 @@
|
|
|
1489
1661
|
}),
|
|
1490
1662
|
showNavigationBarLoading: () => shell.setNavigationLoading(true),
|
|
1491
1663
|
hideNavigationBarLoading: () => shell.setNavigationLoading(false),
|
|
1664
|
+
hideTabBar: () => setTabBarHidden(true),
|
|
1665
|
+
showTabBar: () => setTabBarHidden(false),
|
|
1666
|
+
// 基础库把 removeTabBarBadge、showTabBarRedDot、hideTabBarRedDot 都转成 setTabBarBadge,用 type 区分;
|
|
1667
|
+
// 徽标文字超长时基础库已经换成 ...
|
|
1668
|
+
setTabBarBadge: (args, { api }) => updateTabBarItem(api, args.index, { mark: tabBarMark(args) }),
|
|
1669
|
+
setTabBarItem: (args, { api }) => updateTabBarItem(api, args.index, {
|
|
1670
|
+
text: args.text,
|
|
1671
|
+
iconPath: args.iconPath,
|
|
1672
|
+
selectedIconPath: args.selectedIconPath
|
|
1673
|
+
}),
|
|
1674
|
+
setTabBarStyle: (args) => shell.updateTabBarStyle({
|
|
1675
|
+
color: args.color,
|
|
1676
|
+
selectedColor: args.selectedColor,
|
|
1677
|
+
backgroundColor: args.backgroundColor,
|
|
1678
|
+
borderStyle: args.borderStyle
|
|
1679
|
+
}),
|
|
1492
1680
|
hideHomeButton: () => void 0,
|
|
1493
1681
|
setBackgroundColor: () => void 0,
|
|
1494
1682
|
setBackgroundTextStyle: () => void 0,
|
|
@@ -1496,6 +1684,11 @@
|
|
|
1496
1684
|
stopPullDownRefresh: () => void 0
|
|
1497
1685
|
};
|
|
1498
1686
|
}
|
|
1687
|
+
function tabBarMark(args) {
|
|
1688
|
+
if (args.type === "text") return { type: "text", text: String(args.badgeValue ?? "") };
|
|
1689
|
+
if (args.type === "redDot") return { type: "redDot" };
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1499
1692
|
|
|
1500
1693
|
// src/runtime/host/api/webView.ts
|
|
1501
1694
|
function createWebViewApis({ documentOf, emitToWebview }) {
|
|
@@ -1804,7 +1997,12 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
1804
1997
|
.wxa-tabbar { flex: none; height: calc(50px + var(--wxa-safe-bottom, 0px)); padding-bottom: var(--wxa-safe-bottom, 0px); display: flex; border-top: .5px solid rgba(0,0,0,.1); box-sizing: border-box; }
|
|
1805
1998
|
.wxa-tabbar.is-hidden { display: none; }
|
|
1806
1999
|
.wxa-tabbar-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 10px; cursor: pointer; user-select: none; gap: 2px; }
|
|
1807
|
-
.wxa-tabbar-item img { width: 24px; height: 24px; }
|
|
2000
|
+
.wxa-tabbar-item img { width: 24px; height: 24px; display: block; }
|
|
2001
|
+
.wxa-tabbar-icon, .wxa-tabbar-text { position: relative; }
|
|
2002
|
+
.wxa-tabbar-badge { position: absolute; top: -4px; left: calc(100% - 6px); min-width: 16px; height: 16px; padding: 0 5px; box-sizing: border-box; border-radius: 8px; background: #fa5151; color: #fff; font-size: 11px; line-height: 16px; text-align: center; white-space: nowrap; }
|
|
2003
|
+
.wxa-tabbar-dot { position: absolute; top: -2px; left: calc(100% - 2px); width: 8px; height: 8px; border-radius: 50%; background: #fa5151; }
|
|
2004
|
+
.wxa-tabbar-text > .wxa-tabbar-badge { top: -10px; left: calc(100% + 1px); }
|
|
2005
|
+
.wxa-tabbar-text > .wxa-tabbar-dot { top: -3px; left: calc(100% + 1px); }
|
|
1808
2006
|
.wxa-overlay { position: absolute; inset: 0; pointer-events: none; z-index: 10; }
|
|
1809
2007
|
.wxa-mask { position: absolute; inset: 0; pointer-events: auto; }
|
|
1810
2008
|
.wxa-mask.is-dim { background: rgba(0,0,0,.5); }
|
|
@@ -1969,7 +2167,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
1969
2167
|
this.tabBarConfig = null;
|
|
1970
2168
|
this.onTabTap = () => void 0;
|
|
1971
2169
|
this.navigation = { title: "", backgroundColor: "#000", textStyle: "white", showBack: false, custom: false };
|
|
1972
|
-
|
|
2170
|
+
/** 当前页面是 tabBar 页面时为它的路径 */
|
|
2171
|
+
this.tabBarPath = null;
|
|
2172
|
+
/** wx.hideTabBar 设置,对所有 tab 页生效,直到 wx.showTabBar */
|
|
2173
|
+
this.tabBarHidden = false;
|
|
1973
2174
|
const style = document.createElement("style");
|
|
1974
2175
|
style.textContent = SHELL_CSS;
|
|
1975
2176
|
document.head.appendChild(style);
|
|
@@ -2081,9 +2282,28 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2081
2282
|
setNavigationLoading(loading) {
|
|
2082
2283
|
this.navBar.classList.toggle("is-loading", loading);
|
|
2083
2284
|
}
|
|
2084
|
-
/** 该页面是否显示原生 tabBar(自定义 tabBar
|
|
2285
|
+
/** 该页面是否显示原生 tabBar(自定义 tabBar、被 wx.hideTabBar 隐藏的都不算) */
|
|
2085
2286
|
hasTabBar(pagePath) {
|
|
2086
|
-
return !!this.tabBarConfig?.list.some((item) => item.pagePath === pagePath);
|
|
2287
|
+
return !this.tabBarHidden && !!this.tabBarConfig?.list.some((item) => item.pagePath === pagePath);
|
|
2288
|
+
}
|
|
2289
|
+
setTabBarHidden(hidden) {
|
|
2290
|
+
this.tabBarHidden = hidden;
|
|
2291
|
+
this.renderTabBar(this.tabBarPath);
|
|
2292
|
+
}
|
|
2293
|
+
/** 没有原生 tabBar(未配置或自定义 tabBar)时什么都不做 */
|
|
2294
|
+
updateTabBarStyle(style) {
|
|
2295
|
+
if (!this.tabBarConfig) return;
|
|
2296
|
+
Object.assign(this.tabBarConfig, definedOnly(style));
|
|
2297
|
+
this.renderTabBar(this.tabBarPath);
|
|
2298
|
+
}
|
|
2299
|
+
/** 返回 false 表示 index 越界;没有原生 tabBar 时什么都不做 */
|
|
2300
|
+
updateTabBarItem(index, item) {
|
|
2301
|
+
if (!this.tabBarConfig) return true;
|
|
2302
|
+
const target = this.tabBarConfig.list[index];
|
|
2303
|
+
if (!target) return false;
|
|
2304
|
+
Object.assign(target, definedOnly(item));
|
|
2305
|
+
this.renderTabBar(this.tabBarPath);
|
|
2306
|
+
return true;
|
|
2087
2307
|
}
|
|
2088
2308
|
/**
|
|
2089
2309
|
* 页面窗口里 env(safe-area-inset-*) 的取值:窗口贴着屏幕边缘的一侧才有安全区。
|
|
@@ -2100,9 +2320,10 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2100
2320
|
/** 当前页面是 tabBar 页面时传入它的路径,否则传 null */
|
|
2101
2321
|
renderTabBar(selectedPath) {
|
|
2102
2322
|
const config = this.tabBarConfig;
|
|
2103
|
-
this.
|
|
2104
|
-
|
|
2105
|
-
|
|
2323
|
+
this.tabBarPath = selectedPath;
|
|
2324
|
+
const visible = this.tabBarVisible();
|
|
2325
|
+
this.tabBar.classList.toggle("is-hidden", !visible);
|
|
2326
|
+
if (!config || !visible) return;
|
|
2106
2327
|
this.tabBar.style.background = config.backgroundColor || "#f7f7f7";
|
|
2107
2328
|
this.tabBar.style.borderTopColor = config.borderStyle === "white" ? "rgba(255,255,255,.3)" : "rgba(0,0,0,.1)";
|
|
2108
2329
|
this.tabBar.innerHTML = "";
|
|
@@ -2113,21 +2334,26 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2113
2334
|
el.dataset.pagePath = item.pagePath;
|
|
2114
2335
|
el.style.color = selected ? config.selectedColor || "#07c160" : config.color || "#7a7e83";
|
|
2115
2336
|
const icon = selected ? item.selectedIconPath || item.iconPath : item.iconPath;
|
|
2337
|
+
const iconBox = element("span", "wxa-tabbar-icon");
|
|
2116
2338
|
if (icon) {
|
|
2117
2339
|
const img = document.createElement("img");
|
|
2118
|
-
img.src = "/" + icon.replace(/^\//, "");
|
|
2119
|
-
|
|
2340
|
+
img.src = /^https?:\/\//.test(icon) ? icon : "/" + icon.replace(/^\//, "");
|
|
2341
|
+
iconBox.appendChild(img);
|
|
2342
|
+
el.appendChild(iconBox);
|
|
2120
2343
|
}
|
|
2121
|
-
const text =
|
|
2122
|
-
text.textContent = item.text || "";
|
|
2344
|
+
const text = element("span", "wxa-tabbar-text", item.text || "");
|
|
2123
2345
|
el.appendChild(text);
|
|
2346
|
+
if (item.mark) (icon ? iconBox : text).appendChild(markElement(item.mark));
|
|
2124
2347
|
el.addEventListener("click", () => this.onTabTap(item.pagePath, index));
|
|
2125
2348
|
this.tabBar.appendChild(el);
|
|
2126
2349
|
});
|
|
2127
2350
|
}
|
|
2351
|
+
tabBarVisible() {
|
|
2352
|
+
return this.tabBarPath !== null && this.hasTabBar(this.tabBarPath);
|
|
2353
|
+
}
|
|
2128
2354
|
/** 页面可用高度,与 getSystemInfo().windowHeight 对应;tabBar 会连同底部安全区一起占掉 */
|
|
2129
2355
|
windowHeight() {
|
|
2130
|
-
return this.profile.screenHeight - this.screenTop() - (this.tabBarVisible ? TAB_BAR_HEIGHT + this.profile.safeAreaBottom : 0);
|
|
2356
|
+
return this.profile.screenHeight - this.screenTop() - (this.tabBarVisible() ? TAB_BAR_HEIGHT + this.profile.safeAreaBottom : 0);
|
|
2131
2357
|
}
|
|
2132
2358
|
/** 页面窗口顶部到屏幕顶部的距离,与 getWindowInfo().screenTop 对应 */
|
|
2133
2359
|
screenTop() {
|
|
@@ -2264,6 +2490,12 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2264
2490
|
this.device.querySelector(".wxa-error")?.remove();
|
|
2265
2491
|
}
|
|
2266
2492
|
};
|
|
2493
|
+
function markElement(mark) {
|
|
2494
|
+
return mark.type === "redDot" ? element("span", "wxa-tabbar-dot") : element("span", "wxa-tabbar-badge", mark.text);
|
|
2495
|
+
}
|
|
2496
|
+
function definedOnly(value) {
|
|
2497
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
2498
|
+
}
|
|
2267
2499
|
function maskElement(dim) {
|
|
2268
2500
|
return element("div", "wxa-mask" + (dim ? " is-dim" : ""));
|
|
2269
2501
|
}
|
|
@@ -2285,7 +2517,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2285
2517
|
var BINDING_CANVAS_TYPES = /* @__PURE__ */ new Set(["2d", "webgl", "webgl2"]);
|
|
2286
2518
|
var PROBE_TOP = -54321;
|
|
2287
2519
|
var Host = class {
|
|
2288
|
-
|
|
2520
|
+
/** @param sendReport 把报错发给开发服务器打印到终端,见 AppInfo.forwardConsole */
|
|
2521
|
+
constructor(info, root, sendReport = () => void 0) {
|
|
2289
2522
|
this.info = info;
|
|
2290
2523
|
this.errors = [];
|
|
2291
2524
|
/** 小程序发出的 wx.request */
|
|
@@ -2293,6 +2526,8 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2293
2526
|
this.api = new ApiRegistry((context) => this.reportUnimplemented(context));
|
|
2294
2527
|
/** setup 和测试 API 声明的请求拦截 */
|
|
2295
2528
|
this.routes = new RequestRoutes();
|
|
2529
|
+
/** 把被拦截的请求显示在 Network 面板里 */
|
|
2530
|
+
this.mockMirror = installMockMirror();
|
|
2296
2531
|
this.mapOperations = new MapOperations((webviewId, event, data) => this.publishToPages(event, data, [webviewId]));
|
|
2297
2532
|
this.service = null;
|
|
2298
2533
|
/** 新版基础库的业务子上下文;旧版基础库业务代码直接在逻辑层 iframe 里 */
|
|
@@ -2309,6 +2544,11 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2309
2544
|
/** 渲染层报来的 2d、webgl 画布,按编号记下所在页面和最近的位置,供 placeCanvas 认出逻辑层的画布元素 */
|
|
2310
2545
|
this.canvases = /* @__PURE__ */ new Map();
|
|
2311
2546
|
this.device = info.device;
|
|
2547
|
+
this.forwarder = new ConsoleForwarder(
|
|
2548
|
+
info.forwardConsole ?? false,
|
|
2549
|
+
sendReport,
|
|
2550
|
+
(kind, webviewId) => kind === "service" ? this.currentPage()?.path : this.pages.get(webviewId)?.path
|
|
2551
|
+
);
|
|
2312
2552
|
this.shell = new PhoneShell(root, info.device);
|
|
2313
2553
|
this.tabPaths = new Set((info.wxConfig.tabBar?.list || []).map((item) => stripHtml(item.pagePath)));
|
|
2314
2554
|
this.storage = new MiniProgramStorage(window.localStorage, info.appid);
|
|
@@ -2427,27 +2667,36 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2427
2667
|
}
|
|
2428
2668
|
reportError(kind, webviewId, error) {
|
|
2429
2669
|
const err = typeof error?.message === "string" ? error : null;
|
|
2430
|
-
|
|
2670
|
+
const report = {
|
|
2431
2671
|
kind,
|
|
2432
2672
|
webviewId,
|
|
2433
2673
|
message: err ? err.message : String(error),
|
|
2434
2674
|
stack: typeof err?.stack === "string" ? err.stack : void 0
|
|
2435
|
-
}
|
|
2675
|
+
};
|
|
2676
|
+
this.errors.push(report);
|
|
2677
|
+
this.forwarder.error(kind, webviewId, report.message, report.stack);
|
|
2678
|
+
}
|
|
2679
|
+
get forwardConsole() {
|
|
2680
|
+
return this.forwarder.mode;
|
|
2681
|
+
}
|
|
2682
|
+
reportConsole(kind, webviewId, level, args) {
|
|
2683
|
+
this.forwarder.console(kind, webviewId, level, args);
|
|
2436
2684
|
}
|
|
2437
2685
|
/**
|
|
2438
|
-
*
|
|
2686
|
+
* 只记录逻辑层的公开 wx API 和 FileSystemManager 方法,每个原生接口一次。
|
|
2439
2687
|
* 其余是基础库内部调用的原生接口(如 getPermissionBytes),业务既没调用也 mock 不了
|
|
2440
2688
|
*/
|
|
2441
2689
|
reportUnimplemented({ api, from }) {
|
|
2690
|
+
if (from !== "service") return;
|
|
2442
2691
|
const wx = this.serviceWindow()?.wx;
|
|
2443
|
-
|
|
2692
|
+
const fileMethod = api.startsWith("fs_") ? api.slice(3) : api;
|
|
2693
|
+
const isFileApi = typeof wx?.getFileSystemManager?.()[fileMethod] === "function";
|
|
2694
|
+
if (typeof wx?.[api] !== "function" && !isFileApi) return;
|
|
2444
2695
|
if (this.errors.some((error) => error.api === api)) return;
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
message: `wx.${api} \u5C1A\u672A\u5B9E\u73B0\uFF0C\u8FD4\u56DE\u4E86\u9ED8\u8BA4\u6210\u529F\u3002\u53EF\u4EE5\u7528 mock \u8986\u76D6\uFF0C\u542F\u52A8\u9636\u6BB5\u5C31\u8C03\u7528\u7684\u8981\u5728 setup \u91CC mock`
|
|
2450
|
-
});
|
|
2696
|
+
const name = isFileApi ? `wx.getFileSystemManager().${fileMethod}` : `wx.${api}`;
|
|
2697
|
+
const message = `${name} \u5C1A\u672A\u5B9E\u73B0\uFF0C\u8FD4\u56DE\u4E86\u9ED8\u8BA4\u6210\u529F\u3002\u53EF\u4EE5\u7528 mock \u8986\u76D6\uFF0C\u542F\u52A8\u9636\u6BB5\u5C31\u8C03\u7528\u7684\u8981\u5728 setup \u91CC mock`;
|
|
2698
|
+
this.errors.push({ kind: "service", webviewId: 0, api, message });
|
|
2699
|
+
this.forwarder.warn(message);
|
|
2451
2700
|
}
|
|
2452
2701
|
// ---------------------------------------------------------------------------
|
|
2453
2702
|
// 路由
|
|
@@ -2655,6 +2904,13 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2655
2904
|
page.navigation = { ...page.navigation, ...Object.fromEntries(Object.entries(state).filter(([, v]) => v !== void 0)) };
|
|
2656
2905
|
this.shell.setNavigation(page.navigation);
|
|
2657
2906
|
}
|
|
2907
|
+
/** tabBar 显隐改变 tab 页窗口是否贴着屏幕底边,已打开的 tab 页要同步底部安全区 */
|
|
2908
|
+
setTabBarHidden(hidden) {
|
|
2909
|
+
this.shell.setTabBarHidden(hidden);
|
|
2910
|
+
for (const page of this.tabPages.values()) {
|
|
2911
|
+
page.setSafeAreaInsets(this.shell.safeAreaInsetsFor(page.path, page.navigation));
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2658
2914
|
registerApis() {
|
|
2659
2915
|
const routeApi = (handler) => (args) => this.enqueueRoute(() => handler(args));
|
|
2660
2916
|
this.api.register({
|
|
@@ -2681,12 +2937,17 @@ html, body { margin: 0; height: 100%; background: radial-gradient(120% 80% at 50
|
|
|
2681
2937
|
emit: (event, data) => this.emitToService(event, data),
|
|
2682
2938
|
defaultTimeout: () => this.info.wxConfig.networkTimeout?.request || 6e4,
|
|
2683
2939
|
routes: this.routes,
|
|
2684
|
-
onRequest: (request) => this.requests.add(request)
|
|
2940
|
+
onRequest: (request) => this.requests.add(request),
|
|
2941
|
+
onMocked: (request, response, signal) => this.mockMirror?.show(request, response, signal)
|
|
2685
2942
|
}),
|
|
2686
2943
|
...createAudioApis({ emit: (event, data) => this.emitToService(event, data) }),
|
|
2687
2944
|
...createMapApis({ registry: this.api, operations: this.mapOperations }),
|
|
2688
2945
|
...createLibFileApis({ windowOf: (webviewId) => this.pages.get(webviewId)?.win }),
|
|
2689
|
-
...createUiApis({
|
|
2946
|
+
...createUiApis({
|
|
2947
|
+
shell: this.shell,
|
|
2948
|
+
updateNavigation: (state) => this.updateNavigation(state),
|
|
2949
|
+
setTabBarHidden: (hidden) => this.setTabBarHidden(hidden)
|
|
2950
|
+
}),
|
|
2690
2951
|
...createMediaApis({
|
|
2691
2952
|
showActionSheet: (options) => this.shell.showActionSheet(options),
|
|
2692
2953
|
chooseFiles,
|
|
@@ -3124,14 +3385,14 @@ ${script}
|
|
|
3124
3385
|
const root = document.getElementById("wxa-root");
|
|
3125
3386
|
const info = await (await fetch("/__wxa__/app-info.json")).json();
|
|
3126
3387
|
const restore = new PageStackRestore(info.appid);
|
|
3127
|
-
connectDevServer(restore);
|
|
3388
|
+
const sendReport = connectDevServer(restore);
|
|
3128
3389
|
if (info.buildError) {
|
|
3129
3390
|
new PhoneShell(root, info.device).showError(`\u7F16\u8BD1\u5931\u8D25
|
|
3130
3391
|
|
|
3131
3392
|
${info.buildError}`);
|
|
3132
3393
|
return;
|
|
3133
3394
|
}
|
|
3134
|
-
const host = new Host(info, root);
|
|
3395
|
+
const host = new Host(info, root, sendReport);
|
|
3135
3396
|
window.__wxaHost = host;
|
|
3136
3397
|
let markReady;
|
|
3137
3398
|
const ready = new Promise((resolve) => {
|
|
@@ -3199,6 +3460,13 @@ ${message.message}`);
|
|
|
3199
3460
|
else restore.reload(host);
|
|
3200
3461
|
}
|
|
3201
3462
|
};
|
|
3463
|
+
const pending = [];
|
|
3464
|
+
socket.onopen = () => pending.splice(0).forEach((data) => socket.send(data));
|
|
3465
|
+
return (report) => {
|
|
3466
|
+
const data = JSON.stringify(report);
|
|
3467
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
3468
|
+
else if (socket.readyState === WebSocket.CONNECTING) pending.push(data);
|
|
3469
|
+
};
|
|
3202
3470
|
}
|
|
3203
3471
|
function currentHost() {
|
|
3204
3472
|
return window.__wxaHost;
|