@weapp-vite/miniprogram-automator 1.2.23 → 1.3.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/README.md +22 -0
- package/dist/index.d.mts +36 -3
- package/dist/index.mjs +320 -164
- package/dist/{launch-CSjvON-f.mjs → launch-anJvfi-6.mjs} +1025 -332
- package/dist/operation.d.mts +64 -0
- package/dist/operation.mjs +233 -0
- package/package.json +15 -4
package/dist/index.mjs
CHANGED
|
@@ -1,31 +1,48 @@
|
|
|
1
|
+
import { OPERATION_TIMEOUT_CODE, OperationLifecycle, classifyOperationError, isRecoverableOperationError, readWechatLoginState } from "./operation.mjs";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import net from "node:net";
|
|
3
4
|
import process from "node:process";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { EventEmitter } from "node:events";
|
|
7
7
|
import { createDebug } from "obug";
|
|
8
8
|
import WebSocket from "ws";
|
|
9
9
|
import fs from "node:fs/promises";
|
|
10
10
|
import os from "node:os";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
11
12
|
import { decodeQrCodeFromBase64, renderTerminalQrCode } from "@weapp-vite/qr";
|
|
12
13
|
import { Buffer } from "node:buffer";
|
|
13
14
|
//#region src/internal/compat/async.ts
|
|
14
15
|
/**
|
|
15
16
|
* @file 异步控制工具。
|
|
16
17
|
*/
|
|
17
|
-
/**
|
|
18
|
-
function sleep$1(timeout) {
|
|
19
|
-
|
|
18
|
+
/** 可取消的延迟,取消时释放自身计时器。 */
|
|
19
|
+
function sleep$1(timeout, signal) {
|
|
20
|
+
signal?.throwIfAborted();
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let timer;
|
|
23
|
+
function abort() {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
signal?.removeEventListener("abort", abort);
|
|
26
|
+
reject(signal?.reason);
|
|
27
|
+
}
|
|
28
|
+
timer = setTimeout(() => {
|
|
29
|
+
signal?.removeEventListener("abort", abort);
|
|
30
|
+
resolve();
|
|
31
|
+
}, timeout);
|
|
32
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
33
|
+
});
|
|
20
34
|
}
|
|
21
|
-
/**
|
|
22
|
-
async function waitUntil(condition, timeout = 0, interval = 250) {
|
|
23
|
-
const
|
|
35
|
+
/** 条件轮询受同一个单调时钟 deadline 与取消信号约束。 */
|
|
36
|
+
async function waitUntil(condition, timeout = 0, interval = 250, signal) {
|
|
37
|
+
const deadlineAt = timeout ? performance.now() + timeout : Number.POSITIVE_INFINITY;
|
|
24
38
|
while (true) {
|
|
39
|
+
signal?.throwIfAborted();
|
|
25
40
|
const value = await condition();
|
|
41
|
+
signal?.throwIfAborted();
|
|
26
42
|
if (value) return value;
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
const remaining = deadlineAt - performance.now();
|
|
44
|
+
if (remaining <= 0) throw new Error(`Wait timed out after ${timeout} ms`);
|
|
45
|
+
await sleep$1(Math.min(interval, remaining), signal);
|
|
29
46
|
}
|
|
30
47
|
}
|
|
31
48
|
//#endregion
|
|
@@ -825,7 +842,7 @@ var Transport = class extends EventEmitter {
|
|
|
825
842
|
this.ws.send(message);
|
|
826
843
|
}
|
|
827
844
|
close() {
|
|
828
|
-
this.ws.
|
|
845
|
+
this.ws.terminate();
|
|
829
846
|
}
|
|
830
847
|
};
|
|
831
848
|
//#endregion
|
|
@@ -840,10 +857,17 @@ const CONNECT_TIMEOUT = 3e4;
|
|
|
840
857
|
/** Page frame 定向消息失效、需要切换到 App-service Page 协议的 DevTools 版本。 */
|
|
841
858
|
const APP_SERVICE_PAGE_PROTOCOL_VERSIONS = /* @__PURE__ */ new Set(["2.01.2510290"]);
|
|
842
859
|
/** 仅页面方法调用失效的版本,元素查询仍保留原生组件作用域。 */
|
|
843
|
-
const APP_SERVICE_PAGE_METHOD_VERSIONS = /* @__PURE__ */ new Set([
|
|
860
|
+
const APP_SERVICE_PAGE_METHOD_VERSIONS = /* @__PURE__ */ new Set([
|
|
861
|
+
"2.02.2608070",
|
|
862
|
+
"2.02.2608080",
|
|
863
|
+
"2.02.2609231",
|
|
864
|
+
"2.02.2610082",
|
|
865
|
+
"2.02.2610092"
|
|
866
|
+
]);
|
|
844
867
|
/** Connection 的实现。 */
|
|
845
868
|
var Connection = class Connection extends EventEmitter {
|
|
846
869
|
transport;
|
|
870
|
+
disposed = false;
|
|
847
871
|
callbacks = /* @__PURE__ */ new Map();
|
|
848
872
|
useAppServicePageProtocol = false;
|
|
849
873
|
useAppServicePageMethod = false;
|
|
@@ -854,6 +878,7 @@ var Connection = class Connection extends EventEmitter {
|
|
|
854
878
|
transport.on("close", this.onClose);
|
|
855
879
|
}
|
|
856
880
|
send(method, params = {}, options = {}) {
|
|
881
|
+
if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(closeErrTip));
|
|
857
882
|
const id = uuid();
|
|
858
883
|
const payload = stringify({
|
|
859
884
|
id,
|
|
@@ -877,14 +902,19 @@ var Connection = class Connection extends EventEmitter {
|
|
|
877
902
|
});
|
|
878
903
|
try {
|
|
879
904
|
this.transport.send(payload);
|
|
880
|
-
} catch {
|
|
905
|
+
} catch (cause) {
|
|
881
906
|
clearTimeout(timeout);
|
|
882
907
|
this.callbacks.delete(id);
|
|
883
|
-
reject(
|
|
908
|
+
reject(new Error(closeErrTip, { cause }));
|
|
884
909
|
}
|
|
885
910
|
});
|
|
886
911
|
}
|
|
887
912
|
dispose() {
|
|
913
|
+
if (this.disposed) return;
|
|
914
|
+
this.disposed = true;
|
|
915
|
+
this.onClose();
|
|
916
|
+
this.transport.off("message", this.onMessage);
|
|
917
|
+
this.transport.off("close", this.onClose);
|
|
888
918
|
this.transport.close();
|
|
889
919
|
}
|
|
890
920
|
/** 根据 DevTools 版本选择稳定的 Page 协议实现。 */
|
|
@@ -924,35 +954,53 @@ var Connection = class Connection extends EventEmitter {
|
|
|
924
954
|
}
|
|
925
955
|
this.callbacks.clear();
|
|
926
956
|
};
|
|
927
|
-
static create(url, timeout = CONNECT_TIMEOUT) {
|
|
957
|
+
static create(url, timeout = CONNECT_TIMEOUT, signal) {
|
|
958
|
+
signal?.throwIfAborted();
|
|
928
959
|
return new Promise((resolve, reject) => {
|
|
929
960
|
const ws = new WebSocket(url);
|
|
930
961
|
let settled = false;
|
|
931
|
-
|
|
962
|
+
let timer;
|
|
963
|
+
let onAbort;
|
|
964
|
+
let onOpen;
|
|
965
|
+
let onClose;
|
|
966
|
+
function cleanup() {
|
|
967
|
+
clearTimeout(timer);
|
|
968
|
+
signal?.removeEventListener("abort", onAbort);
|
|
969
|
+
ws.off("open", onOpen);
|
|
970
|
+
ws.off("close", onClose);
|
|
971
|
+
}
|
|
972
|
+
function fail(error) {
|
|
932
973
|
if (settled) return;
|
|
933
974
|
settled = true;
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
975
|
+
cleanup();
|
|
976
|
+
ws.terminate();
|
|
977
|
+
reject(error);
|
|
978
|
+
}
|
|
979
|
+
onAbort = () => {
|
|
980
|
+
fail(signal?.reason);
|
|
981
|
+
};
|
|
982
|
+
onClose = () => {
|
|
983
|
+
fail(/* @__PURE__ */ new Error("DevTools websocket closed before handshake completed"));
|
|
984
|
+
};
|
|
985
|
+
onOpen = () => {
|
|
938
986
|
if (settled) return;
|
|
939
987
|
settled = true;
|
|
940
|
-
|
|
988
|
+
cleanup();
|
|
941
989
|
resolve(new Connection(new Transport(ws)));
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
990
|
+
};
|
|
991
|
+
timer = setTimeout(() => fail(/* @__PURE__ */ new Error(`Timed out connecting to DevTools websocket ${url} after ${timeout}ms`)), timeout);
|
|
992
|
+
ws.on("open", onOpen);
|
|
993
|
+
ws.on("error", fail);
|
|
994
|
+
ws.on("close", onClose);
|
|
995
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
996
|
+
if (signal?.aborted) onAbort();
|
|
949
997
|
});
|
|
950
998
|
}
|
|
951
999
|
};
|
|
952
1000
|
//#endregion
|
|
953
1001
|
//#region src/headless.ts
|
|
954
1002
|
async function launchHeadlessAutomator(options) {
|
|
955
|
-
return await (await import("./launch-
|
|
1003
|
+
return await (await import("./launch-anJvfi-6.mjs")).launch({ projectPath: options.projectPath });
|
|
956
1004
|
}
|
|
957
1005
|
//#endregion
|
|
958
1006
|
//#region src/launcher/portLease.ts
|
|
@@ -1064,7 +1112,10 @@ async function enableAutomatorViaHttp(options) {
|
|
|
1064
1112
|
if (options.account) endpoint.searchParams.set("account", options.account);
|
|
1065
1113
|
if (options.ticket) endpoint.searchParams.set("ticket", options.ticket);
|
|
1066
1114
|
if (options.trustProject) endpoint.searchParams.set("trustProject", "true");
|
|
1067
|
-
const response = await fetch(endpoint, {
|
|
1115
|
+
const response = await fetch(endpoint, {
|
|
1116
|
+
redirect: "follow",
|
|
1117
|
+
signal: options.signal
|
|
1118
|
+
});
|
|
1068
1119
|
const body = await response.text();
|
|
1069
1120
|
if (!response.ok) throw new Error(`WeChat DevTools HTTP automator fallback failed with status ${response.status}`);
|
|
1070
1121
|
let result;
|
|
@@ -1073,6 +1124,7 @@ async function enableAutomatorViaHttp(options) {
|
|
|
1073
1124
|
} catch {
|
|
1074
1125
|
throw new Error("WeChat DevTools HTTP automator fallback returned invalid JSON");
|
|
1075
1126
|
}
|
|
1127
|
+
if (result && typeof result === "object" && "code" in result && Number(result.code) === 10) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_LOGIN_REQUIRED: need re-login"), { code: 10 });
|
|
1076
1128
|
if (result && typeof result === "object" && "autoPort" in result) {
|
|
1077
1129
|
const autoPort = Number(result.autoPort);
|
|
1078
1130
|
if (!Number.isInteger(autoPort) || autoPort <= 0 || autoPort > 65535) throw new Error("WeChat DevTools HTTP automator fallback returned invalid autoPort");
|
|
@@ -1106,8 +1158,122 @@ function resolveWechatDevtoolsBootstrapArgs(args) {
|
|
|
1106
1158
|
return bootstrapArgs;
|
|
1107
1159
|
}
|
|
1108
1160
|
//#endregion
|
|
1161
|
+
//#region src/launcher/process.ts
|
|
1162
|
+
const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
|
|
1163
|
+
function shouldUseWindowsCommandShell(cliPath) {
|
|
1164
|
+
return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
|
|
1165
|
+
}
|
|
1166
|
+
function escapeWindowsCmdArg(arg) {
|
|
1167
|
+
const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
|
|
1168
|
+
return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
|
|
1169
|
+
}
|
|
1170
|
+
function resolveWindowsBatchSpawn(cliPath, args) {
|
|
1171
|
+
return {
|
|
1172
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
1173
|
+
args: [
|
|
1174
|
+
"/d",
|
|
1175
|
+
"/s",
|
|
1176
|
+
"/c",
|
|
1177
|
+
`"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
|
|
1178
|
+
]
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/** 只持有本次 CLI 子进程句柄;退出后不通过旧 PID 查找或终止宿主。 */
|
|
1182
|
+
function spawnWechatCli(cliPath, args, cwd, scope) {
|
|
1183
|
+
scope.throwIfAborted();
|
|
1184
|
+
const state = {
|
|
1185
|
+
error: null,
|
|
1186
|
+
success: false,
|
|
1187
|
+
exited: false,
|
|
1188
|
+
output: ""
|
|
1189
|
+
};
|
|
1190
|
+
const bootstrapArgs = resolveWechatDevtoolsBootstrapArgs(args);
|
|
1191
|
+
const target = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, bootstrapArgs) : {
|
|
1192
|
+
file: cliPath,
|
|
1193
|
+
args: bootstrapArgs
|
|
1194
|
+
};
|
|
1195
|
+
const child = spawn(target.file, target.args, {
|
|
1196
|
+
stdio: [
|
|
1197
|
+
"ignore",
|
|
1198
|
+
"pipe",
|
|
1199
|
+
"pipe"
|
|
1200
|
+
],
|
|
1201
|
+
cwd: cwd || void 0,
|
|
1202
|
+
...shouldUseWindowsCommandShell(cliPath) ? {
|
|
1203
|
+
windowsHide: true,
|
|
1204
|
+
windowsVerbatimArguments: true
|
|
1205
|
+
} : {}
|
|
1206
|
+
});
|
|
1207
|
+
let released;
|
|
1208
|
+
const release = () => {
|
|
1209
|
+
if (!released) released = new Promise((resolve) => {
|
|
1210
|
+
if (state.exited || !child.pid || child.exitCode !== null || child.signalCode !== null) {
|
|
1211
|
+
resolve();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
child.once("close", () => resolve());
|
|
1215
|
+
child.kill("SIGKILL");
|
|
1216
|
+
});
|
|
1217
|
+
return released;
|
|
1218
|
+
};
|
|
1219
|
+
const disown = scope.own(release, "cli-process");
|
|
1220
|
+
child.on("error", (error) => {
|
|
1221
|
+
state.error = error;
|
|
1222
|
+
});
|
|
1223
|
+
const output = (chunk) => {
|
|
1224
|
+
state.output = (state.output + String(chunk)).slice(-32768);
|
|
1225
|
+
};
|
|
1226
|
+
child.stdout?.on("data", output);
|
|
1227
|
+
child.stderr?.on("data", output);
|
|
1228
|
+
child.on("exit", (code, signal) => {
|
|
1229
|
+
disown();
|
|
1230
|
+
state.exited = true;
|
|
1231
|
+
if (code !== 0 || signal) state.error = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
|
|
1232
|
+
else state.success = true;
|
|
1233
|
+
});
|
|
1234
|
+
child.unref();
|
|
1235
|
+
return Object.assign(state, { release });
|
|
1236
|
+
}
|
|
1237
|
+
//#endregion
|
|
1109
1238
|
//#region package.json
|
|
1110
|
-
var version = "1.
|
|
1239
|
+
var version = "1.3.0";
|
|
1240
|
+
//#endregion
|
|
1241
|
+
//#region src/appServiceHeap.ts
|
|
1242
|
+
function isRecord$1(value) {
|
|
1243
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1244
|
+
}
|
|
1245
|
+
function unsupported$1(reason) {
|
|
1246
|
+
return {
|
|
1247
|
+
status: "unsupported",
|
|
1248
|
+
source: "appservice-cdp-runtime",
|
|
1249
|
+
reason
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
/** 只读取 AppService 当前 V8 堆,不调用 GC,也不把协议故障或空响应当成零内存。 */
|
|
1253
|
+
async function readAppServiceHeapUsage(send, options = {}) {
|
|
1254
|
+
const timeout = options.timeout ?? 2500;
|
|
1255
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new TypeError("AppService heap timeout must be finite and positive");
|
|
1256
|
+
let response;
|
|
1257
|
+
try {
|
|
1258
|
+
response = await send("App.CDPCommand", {
|
|
1259
|
+
domain: "Runtime",
|
|
1260
|
+
method: "getHeapUsage",
|
|
1261
|
+
params: {}
|
|
1262
|
+
}, { timeout });
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (error instanceof Error && (error.message === "appservice App.CDPCommand unimplemented" || error.message === "Method not implemented.")) return unsupported$1("protocol-unimplemented");
|
|
1265
|
+
if (error instanceof Error && error.message === "'Runtime.getHeapUsage' wasn't found") return unsupported$1("method-not-found");
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
if (isRecord$1(response) && isRecord$1(response.error) && response.error.code === -32601) return unsupported$1("method-not-found");
|
|
1269
|
+
if (!isRecord$1(response) || response.error || response.exceptionDetails || typeof response.usedSize !== "number" || !Number.isFinite(response.usedSize) || response.usedSize < 0 || typeof response.totalSize !== "number" || !Number.isFinite(response.totalSize) || response.totalSize < response.usedSize) throw new Error("Invalid AppService Runtime.getHeapUsage response: expected finite non-negative usedSize <= totalSize");
|
|
1270
|
+
return {
|
|
1271
|
+
status: "available",
|
|
1272
|
+
source: "appservice-cdp-runtime",
|
|
1273
|
+
usedSize: response.usedSize,
|
|
1274
|
+
totalSize: response.totalSize
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1111
1277
|
//#endregion
|
|
1112
1278
|
//#region src/Native.ts
|
|
1113
1279
|
/** Native 的实现。 */
|
|
@@ -1158,6 +1324,25 @@ var Native = class extends EventEmitter {
|
|
|
1158
1324
|
}
|
|
1159
1325
|
};
|
|
1160
1326
|
//#endregion
|
|
1327
|
+
//#region src/navigation/query.ts
|
|
1328
|
+
/** 仅核对请求显式携带的参数,保留宿主附加参数及其原始编码形式。 */
|
|
1329
|
+
function matchesRouteQuery(actual, expected) {
|
|
1330
|
+
const expectedKeys = Object.keys(expected);
|
|
1331
|
+
if (!expectedKeys.length) return true;
|
|
1332
|
+
if (!actual || typeof actual !== "object" || Array.isArray(actual)) return false;
|
|
1333
|
+
const query = actual;
|
|
1334
|
+
return expectedKeys.every((key) => {
|
|
1335
|
+
if (!Object.prototype.hasOwnProperty.call(query, key) || query[key] == null) return false;
|
|
1336
|
+
const value = String(query[key]);
|
|
1337
|
+
if (value === expected[key]) return true;
|
|
1338
|
+
try {
|
|
1339
|
+
return decodeURIComponent(value) === expected[key];
|
|
1340
|
+
} catch {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1161
1346
|
//#region src/pageRouteFallback.ts
|
|
1162
1347
|
function normalizeFallbackIdPart(value) {
|
|
1163
1348
|
return encodeURIComponent(value).replace(/%/g, "_");
|
|
@@ -2422,7 +2607,7 @@ function isPluginNavigationUrl(value) {
|
|
|
2422
2607
|
}
|
|
2423
2608
|
function matchesRouteChange(options) {
|
|
2424
2609
|
const normalizedCurrentPath = normalizeRoutePath(options.currentPath);
|
|
2425
|
-
if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return
|
|
2610
|
+
if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return matchesRouteQuery(options.currentQuery, options.expectedQuery);
|
|
2426
2611
|
if (!isPluginNavigationUrl(options.requestedUrl) || !isPluginPath(options.currentPath)) return false;
|
|
2427
2612
|
return normalizedCurrentPath !== normalizeRoutePath(options.previousPath);
|
|
2428
2613
|
}
|
|
@@ -2600,6 +2785,10 @@ var MiniProgram = class extends EventEmitter {
|
|
|
2600
2785
|
}, sendOptions);
|
|
2601
2786
|
return result;
|
|
2602
2787
|
}
|
|
2788
|
+
/** 探测并读取 AppService JS 堆;明确不支持时返回原因,连接故障与无效响应继续抛出。 */
|
|
2789
|
+
async getAppServiceHeapUsage(options = {}) {
|
|
2790
|
+
return await readAppServiceHeapUsage((method, params, sendOptions) => this.send(method, params, sendOptions), options);
|
|
2791
|
+
}
|
|
2603
2792
|
async pageScrollTo(scrollTop) {
|
|
2604
2793
|
await this.callWxMethod("pageScrollTo", {
|
|
2605
2794
|
scrollTop,
|
|
@@ -2695,7 +2884,7 @@ var MiniProgram = class extends EventEmitter {
|
|
|
2695
2884
|
if (sdkVersion !== "dev" && cmpVersion(sdkVersion, "2.7.3") < 0) throw new Error(`SDKVersion is currently ${sdkVersion}, while automator(${version}) requires at least version 2.7.3`);
|
|
2696
2885
|
}
|
|
2697
2886
|
/**
|
|
2698
|
-
* @description
|
|
2887
|
+
* @description 等待原生协议返回真实页面标识和路径,避免返回半就绪自动化会话。
|
|
2699
2888
|
*/
|
|
2700
2889
|
async waitForAppReady(timeout = APP_READY_TIMEOUT) {
|
|
2701
2890
|
const startedAt = Date.now();
|
|
@@ -2703,13 +2892,15 @@ var MiniProgram = class extends EventEmitter {
|
|
|
2703
2892
|
while (Date.now() - startedAt < timeout) {
|
|
2704
2893
|
const remainingTimeout = timeout - (Date.now() - startedAt);
|
|
2705
2894
|
try {
|
|
2706
|
-
await this.send("App.
|
|
2707
|
-
return;
|
|
2895
|
+
const page = await this.send("App.getCurrentPage", {}, { timeout: Math.min(APP_READY_PROBE_TIMEOUT, remainingTimeout) });
|
|
2896
|
+
if (page && typeof page === "object" && "pageId" in page && typeof page.pageId === "number" && Number.isSafeInteger(page.pageId) && page.pageId >= 0 && "path" in page && typeof page.path === "string" && page.path.trim()) return;
|
|
2897
|
+
lastError = /* @__PURE__ */ new Error("App.getCurrentPage returned no ready page with a valid pageId and path");
|
|
2708
2898
|
} catch (error) {
|
|
2899
|
+
if (!isCurrentPageProtocolTimeout(error) && !isPageMetaMissingError(error) && !isCurrentFrameTimedOutError(error)) throw error;
|
|
2709
2900
|
lastError = error;
|
|
2710
|
-
const remainingAfterProbe = timeout - (Date.now() - startedAt);
|
|
2711
|
-
if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
|
|
2712
2901
|
}
|
|
2902
|
+
const remainingAfterProbe = timeout - (Date.now() - startedAt);
|
|
2903
|
+
if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
|
|
2713
2904
|
}
|
|
2714
2905
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Timed out waiting App domain readiness after ${timeout}ms`);
|
|
2715
2906
|
}
|
|
@@ -2800,9 +2991,11 @@ var MiniProgram = class extends EventEmitter {
|
|
|
2800
2991
|
logChangeRouteDebug(`poll method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
|
|
2801
2992
|
if (matchesRouteChange({
|
|
2802
2993
|
currentPath: page?.path,
|
|
2994
|
+
currentQuery: page?.query,
|
|
2803
2995
|
previousPath: currentPage?.path,
|
|
2804
2996
|
requestedUrl: url,
|
|
2805
|
-
expectedRoute
|
|
2997
|
+
expectedRoute,
|
|
2998
|
+
expectedQuery: expectedRouteQuery
|
|
2806
2999
|
})) {
|
|
2807
3000
|
logChangeRouteDebug(`ready method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
|
|
2808
3001
|
return page;
|
|
@@ -2819,9 +3012,11 @@ var MiniProgram = class extends EventEmitter {
|
|
|
2819
3012
|
logChangeRouteDebug(`stack method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
|
|
2820
3013
|
if (matchesRouteChange({
|
|
2821
3014
|
currentPath: stackTop.path,
|
|
3015
|
+
currentQuery: stackTop.query,
|
|
2822
3016
|
previousPath: currentPage?.path,
|
|
2823
3017
|
requestedUrl: url,
|
|
2824
|
-
expectedRoute
|
|
3018
|
+
expectedRoute,
|
|
3019
|
+
expectedQuery: expectedRouteQuery
|
|
2825
3020
|
})) {
|
|
2826
3021
|
logChangeRouteDebug(`stack-ready method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
|
|
2827
3022
|
return stackTop;
|
|
@@ -3364,16 +3559,7 @@ const VERSION_CHECK_TIMEOUT = 3e4;
|
|
|
3364
3559
|
const AUTOMATOR_LAUNCH_RETRIES = 3;
|
|
3365
3560
|
const DEFAULT_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER";
|
|
3366
3561
|
const LEGACY_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_E2E_RUNTIME_PROVIDER";
|
|
3367
|
-
const EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
|
|
3368
|
-
const RETRYABLE_LAUNCH_PORT_RE = /Wait timed out after \d+ ms|Failed connecting to ws:\/\/127\.0\.0\.1:\d+|Failed connecting to devtools websocket endpoint|Failed to launch wechat web devTools, please make sure cliPath is correctly specified/i;
|
|
3369
|
-
const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
|
|
3370
3562
|
let localhostListenPatched = false;
|
|
3371
|
-
function isExtensionContextInvalidatedError(error) {
|
|
3372
|
-
return error instanceof Error && EXTENSION_CONTEXT_INVALIDATED_RE.test(error.message);
|
|
3373
|
-
}
|
|
3374
|
-
function isRetryableAutomatorPortLaunchError(error) {
|
|
3375
|
-
return error instanceof Error && RETRYABLE_LAUNCH_PORT_RE.test(error.message);
|
|
3376
|
-
}
|
|
3377
3563
|
function retainPortLeaseUntilSessionClose(miniProgram, portLease) {
|
|
3378
3564
|
let released = false;
|
|
3379
3565
|
const release = async () => {
|
|
@@ -3418,25 +3604,6 @@ function patchNetListenToLoopback() {
|
|
|
3418
3604
|
return rawListen.apply(this, args);
|
|
3419
3605
|
};
|
|
3420
3606
|
}
|
|
3421
|
-
/** IConnectOptions 的类型定义。 */
|
|
3422
|
-
function shouldUseWindowsCommandShell(cliPath) {
|
|
3423
|
-
return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
|
|
3424
|
-
}
|
|
3425
|
-
function escapeWindowsCmdArg(arg) {
|
|
3426
|
-
const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
|
|
3427
|
-
return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
|
|
3428
|
-
}
|
|
3429
|
-
function resolveWindowsBatchSpawn(cliPath, args) {
|
|
3430
|
-
return {
|
|
3431
|
-
file: process.env.ComSpec || "cmd.exe",
|
|
3432
|
-
args: [
|
|
3433
|
-
"/d",
|
|
3434
|
-
"/s",
|
|
3435
|
-
"/c",
|
|
3436
|
-
`"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
|
|
3437
|
-
]
|
|
3438
|
-
};
|
|
3439
|
-
}
|
|
3440
3607
|
function resolveRuntimeProvider(options) {
|
|
3441
3608
|
return options.runtimeProvider || process.env[DEFAULT_RUNTIME_PROVIDER_ENV] || process.env[LEGACY_RUNTIME_PROVIDER_ENV] || "devtools";
|
|
3442
3609
|
}
|
|
@@ -3449,88 +3616,62 @@ var Launcher = class {
|
|
|
3449
3616
|
return await launchHeadlessAutomator({ projectPath: options.projectPath });
|
|
3450
3617
|
}
|
|
3451
3618
|
patchNetListenToLoopback();
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3619
|
+
return await new OperationLifecycle(options.timeout ?? DEFAULT_TIMEOUT, "automator launch", options.signal).run(async (scope) => {
|
|
3620
|
+
const attempts = options.port ? 1 : AUTOMATOR_LAUNCH_RETRIES;
|
|
3621
|
+
for (let attempt = 1;; attempt += 1) {
|
|
3622
|
+
scope.attempt();
|
|
3623
|
+
try {
|
|
3624
|
+
return await scope.step(() => this.launchWechatDevtools(options, scope));
|
|
3625
|
+
} catch (error) {
|
|
3626
|
+
scope.recordFailure(error);
|
|
3627
|
+
scope.throwIfAborted();
|
|
3628
|
+
if (!isRecoverableOperationError(error) || attempt >= attempts) throw error;
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3461
3632
|
}
|
|
3462
|
-
async launchWechatDevtools(options) {
|
|
3463
|
-
const { cliPath = await this.resolveCliPath(),
|
|
3464
|
-
|
|
3465
|
-
const portLease = await acquireAutomatorPortLease(options.port)
|
|
3633
|
+
async launchWechatDevtools(options, scope) {
|
|
3634
|
+
const { cliPath = await scope.step(() => this.resolveCliPath(), { stage: "resolve-cli" }), projectConfig = {}, ticket = "", cwd = "", account = "", trustProject = false } = options;
|
|
3635
|
+
const { args = [], projectPath } = options;
|
|
3636
|
+
const portLease = await scope.step(() => acquireAutomatorPortLease(options.port), {
|
|
3637
|
+
stage: "port-lease",
|
|
3638
|
+
disposeLate: (lease) => lease.release()
|
|
3639
|
+
});
|
|
3640
|
+
const rawReleaseLease = portLease.release.bind(portLease);
|
|
3641
|
+
let releasedLease;
|
|
3642
|
+
portLease.release = () => releasedLease ??= rawReleaseLease();
|
|
3643
|
+
const disownLease = scope.own(() => portLease.release(), "port-lease");
|
|
3466
3644
|
let releasePortLeaseOnExit = true;
|
|
3645
|
+
let releaseChild = async () => {};
|
|
3467
3646
|
try {
|
|
3468
3647
|
const port = portLease.port;
|
|
3469
3648
|
if (!cliPath) throw new Error("Wechat web devTools not found, please specify cliPath option");
|
|
3470
3649
|
if (isWindows && endWith(cliPath, ".exe")) throw new Error("cliPath is not correct, it's usually named as 'cli' or 'cli.bat'");
|
|
3471
3650
|
if (!projectPath) throw new Error("projectPath is not provided");
|
|
3472
3651
|
const resolvedProjectPath = isRelative(projectPath) ? path.resolve(projectPath) : projectPath;
|
|
3473
|
-
if (
|
|
3474
|
-
if (!
|
|
3475
|
-
|
|
3476
|
-
let processError = null;
|
|
3477
|
-
let processExitCode = null;
|
|
3478
|
-
let processSignal = null;
|
|
3479
|
-
let successfulCliExitSettled = false;
|
|
3652
|
+
if (!await scope.step(() => import("node:fs/promises").then((fs) => fs.access(resolvedProjectPath).then(() => true).catch(() => false)), { stage: "project" })) throw new Error(`Project path ${resolvedProjectPath} doesn't exist`);
|
|
3653
|
+
if (!isEmpty(projectConfig)) await scope.step(() => this.extendProjectConfig(projectConfig, resolvedProjectPath), { stage: "project-config" });
|
|
3654
|
+
scope.throwIfAborted();
|
|
3480
3655
|
let httpFallbackAttempted = false;
|
|
3481
3656
|
let httpFallbackError = null;
|
|
3482
3657
|
let targetPort = port;
|
|
3483
|
-
const
|
|
3484
|
-
|
|
3485
|
-
try {
|
|
3486
|
-
const spawnTarget = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, args) : {
|
|
3487
|
-
file: cliPath,
|
|
3488
|
-
args
|
|
3489
|
-
};
|
|
3490
|
-
const child = spawn(spawnTarget.file, spawnTarget.args, {
|
|
3491
|
-
stdio: [
|
|
3492
|
-
"ignore",
|
|
3493
|
-
"pipe",
|
|
3494
|
-
"pipe"
|
|
3495
|
-
],
|
|
3496
|
-
cwd: cwd || void 0,
|
|
3497
|
-
...shouldUseWindowsCommandShell(cliPath) ? {
|
|
3498
|
-
windowsHide: true,
|
|
3499
|
-
windowsVerbatimArguments: true
|
|
3500
|
-
} : {}
|
|
3501
|
-
});
|
|
3502
|
-
child.on("error", (error) => {
|
|
3503
|
-
processError = error;
|
|
3504
|
-
});
|
|
3505
|
-
child.stdout?.on("data", (chunk) => {
|
|
3506
|
-
cliOutput.push(String(chunk));
|
|
3507
|
-
});
|
|
3508
|
-
child.stderr?.on("data", (chunk) => {
|
|
3509
|
-
cliOutput.push(String(chunk));
|
|
3510
|
-
});
|
|
3511
|
-
child.on("exit", (code, signal) => {
|
|
3512
|
-
processExitCode = code;
|
|
3513
|
-
processSignal = signal;
|
|
3514
|
-
if (code !== 0 || signal) processError = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
|
|
3515
|
-
else successfulCliExitSettled = true;
|
|
3516
|
-
});
|
|
3517
|
-
child.unref();
|
|
3518
|
-
} catch (error) {
|
|
3519
|
-
processError = error;
|
|
3520
|
-
}
|
|
3658
|
+
const cli = spawnWechatCli(cliPath, args, cwd, scope);
|
|
3659
|
+
releaseChild = cli.release;
|
|
3521
3660
|
let miniProgram = null;
|
|
3522
3661
|
let lastConnectError = null;
|
|
3523
|
-
const
|
|
3524
|
-
|
|
3525
|
-
|
|
3662
|
+
const resolveRemainingTimeout = () => scope.remainingMs();
|
|
3663
|
+
await scope.step(() => waitUntil(async () => {
|
|
3664
|
+
scope.throwIfAborted();
|
|
3526
3665
|
try {
|
|
3527
|
-
if (
|
|
3528
|
-
if (
|
|
3529
|
-
|
|
3666
|
+
if (cli.error) return true;
|
|
3667
|
+
if (cli.success && readWechatLoginState(cli.output) === false) throw Object.assign(/* @__PURE__ */ new Error("DEVTOOLS_LOGIN_REQUIRED: need re-login"), { code: 10 });
|
|
3668
|
+
if (cli.success && !httpFallbackAttempted) {
|
|
3669
|
+
const servicePort = extractWechatDevtoolsServicePort(cli.output);
|
|
3530
3670
|
if (servicePort) {
|
|
3531
3671
|
httpFallbackAttempted = true;
|
|
3532
3672
|
try {
|
|
3533
3673
|
targetPort = await enableAutomatorViaHttp({
|
|
3674
|
+
signal: scope.signal,
|
|
3534
3675
|
account,
|
|
3535
3676
|
autoPort: port,
|
|
3536
3677
|
projectPath: resolvedProjectPath,
|
|
@@ -3546,43 +3687,59 @@ var Launcher = class {
|
|
|
3546
3687
|
}
|
|
3547
3688
|
const connectTimeout = resolveRemainingTimeout();
|
|
3548
3689
|
if (connectTimeout <= 0) return false;
|
|
3549
|
-
const candidate = await this.connectTool({
|
|
3690
|
+
const candidate = await scope.step(() => this.connectTool({
|
|
3691
|
+
signal: scope.signal,
|
|
3550
3692
|
timeout: Math.min(3e3, connectTimeout),
|
|
3551
3693
|
wsEndpoint: `ws://127.0.0.1:${targetPort}`
|
|
3694
|
+
}), {
|
|
3695
|
+
stage: "websocket",
|
|
3696
|
+
disposeLate: (program) => program.disconnect()
|
|
3552
3697
|
});
|
|
3698
|
+
let releasedCandidate = false;
|
|
3699
|
+
const releaseCandidate = () => {
|
|
3700
|
+
if (!releasedCandidate) {
|
|
3701
|
+
releasedCandidate = true;
|
|
3702
|
+
candidate.disconnect();
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
const disownCandidate = scope.own(releaseCandidate, "websocket");
|
|
3553
3706
|
try {
|
|
3554
3707
|
const checkVersionTimeout = resolveRemainingTimeout();
|
|
3555
3708
|
if (checkVersionTimeout <= 0) {
|
|
3556
|
-
|
|
3709
|
+
releaseCandidate();
|
|
3557
3710
|
return false;
|
|
3558
3711
|
}
|
|
3559
|
-
await candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout));
|
|
3712
|
+
await scope.step(() => candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout)), { stage: "version" });
|
|
3560
3713
|
if (typeof candidate.waitForAppReady === "function") {
|
|
3561
3714
|
const appReadyTimeout = resolveRemainingTimeout();
|
|
3562
3715
|
if (appReadyTimeout <= 0) {
|
|
3563
|
-
|
|
3716
|
+
releaseCandidate();
|
|
3564
3717
|
return false;
|
|
3565
3718
|
}
|
|
3566
|
-
await candidate.waitForAppReady(appReadyTimeout);
|
|
3719
|
+
await scope.step(() => candidate.waitForAppReady(appReadyTimeout), { stage: "app-ready" });
|
|
3567
3720
|
}
|
|
3568
3721
|
} catch (error) {
|
|
3569
|
-
|
|
3722
|
+
disownCandidate();
|
|
3723
|
+
releaseCandidate();
|
|
3570
3724
|
lastConnectError = error;
|
|
3571
|
-
if (
|
|
3725
|
+
if (isRecoverableOperationError(error)) return false;
|
|
3572
3726
|
throw error;
|
|
3573
3727
|
}
|
|
3574
3728
|
miniProgram = candidate;
|
|
3575
3729
|
return true;
|
|
3576
3730
|
} catch (error) {
|
|
3577
3731
|
lastConnectError = error;
|
|
3732
|
+
scope.recordFailure(error);
|
|
3733
|
+
scope.throwIfAborted();
|
|
3734
|
+
if (!isRecoverableOperationError(error)) throw error;
|
|
3578
3735
|
return false;
|
|
3579
3736
|
}
|
|
3580
|
-
},
|
|
3737
|
+
}, scope.remainingMs(), 1e3, scope.signal), { stage: "readiness" });
|
|
3581
3738
|
if (!miniProgram) {
|
|
3582
3739
|
if (httpFallbackError) throw httpFallbackError;
|
|
3583
|
-
if (
|
|
3740
|
+
if (cli.error) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified", { cause: cli.error });
|
|
3584
3741
|
if (lastConnectError) throw lastConnectError;
|
|
3585
|
-
if (
|
|
3742
|
+
if (cli.exited) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
|
|
3586
3743
|
throw new Error("Failed connecting to devtools websocket endpoint");
|
|
3587
3744
|
}
|
|
3588
3745
|
const resolvedMiniProgram = miniProgram;
|
|
@@ -3592,33 +3749,32 @@ var Launcher = class {
|
|
|
3592
3749
|
wsEndpoint: `ws://127.0.0.1:${targetPort}`
|
|
3593
3750
|
});
|
|
3594
3751
|
releasePortLeaseOnExit = !retainPortLeaseUntilSessionClose(resolvedMiniProgram, portLease);
|
|
3595
|
-
|
|
3752
|
+
disownLease();
|
|
3596
3753
|
return resolvedMiniProgram;
|
|
3597
3754
|
} finally {
|
|
3598
|
-
|
|
3755
|
+
await releaseChild();
|
|
3756
|
+
if (releasePortLeaseOnExit) {
|
|
3757
|
+
disownLease();
|
|
3758
|
+
await portLease.release();
|
|
3759
|
+
}
|
|
3599
3760
|
}
|
|
3600
3761
|
}
|
|
3601
3762
|
async connect(options) {
|
|
3602
3763
|
if (normalizePlatform(options.platform) === "swan") return await new SwanLauncher().connect(options);
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3764
|
+
return await new OperationLifecycle(options.timeout ?? DEFAULT_TIMEOUT, "automator connect", options.signal).run(async (scope) => {
|
|
3765
|
+
scope.attempt();
|
|
3766
|
+
const miniProgram = await scope.step(() => this.connectTool({
|
|
3767
|
+
...options,
|
|
3768
|
+
signal: scope.signal,
|
|
3769
|
+
timeout: scope.remainingMs()
|
|
3770
|
+
}), {
|
|
3771
|
+
stage: "websocket",
|
|
3772
|
+
disposeLate: (program) => program.disconnect()
|
|
3773
|
+
});
|
|
3774
|
+
scope.own(() => miniProgram.disconnect(), "websocket");
|
|
3775
|
+
await scope.step(() => miniProgram.checkVersion(scope.remainingMs()), { stage: "version" });
|
|
3613
3776
|
return miniProgram;
|
|
3614
|
-
}
|
|
3615
|
-
try {
|
|
3616
|
-
miniProgram.disconnect();
|
|
3617
|
-
} catch (cleanupError) {
|
|
3618
|
-
throw new AggregateError([error, cleanupError], "Automator connection and cleanup both failed", { cause: error });
|
|
3619
|
-
}
|
|
3620
|
-
throw error;
|
|
3621
|
-
}
|
|
3777
|
+
});
|
|
3622
3778
|
}
|
|
3623
3779
|
async extendProjectConfig(projectConfig, projectPath) {
|
|
3624
3780
|
const projectConfigPath = path.resolve(projectPath, "project.config.json");
|
|
@@ -3630,9 +3786,9 @@ var Launcher = class {
|
|
|
3630
3786
|
}
|
|
3631
3787
|
async connectTool(options) {
|
|
3632
3788
|
try {
|
|
3633
|
-
return new MiniProgram(await Connection.create(options.wsEndpoint, options.timeout));
|
|
3634
|
-
} catch {
|
|
3635
|
-
throw new Error(`Failed connecting to ${options.wsEndpoint}, check if target project window is opened with automation enabled
|
|
3789
|
+
return new MiniProgram(await Connection.create(options.wsEndpoint, options.timeout, options.signal));
|
|
3790
|
+
} catch (cause) {
|
|
3791
|
+
throw new Error(`Failed connecting to ${options.wsEndpoint}, check if target project window is opened with automation enabled`, { cause });
|
|
3636
3792
|
}
|
|
3637
3793
|
}
|
|
3638
3794
|
async resolveCliPath() {
|
|
@@ -3674,4 +3830,4 @@ var Automator = class {
|
|
|
3674
3830
|
*/
|
|
3675
3831
|
const SmartappAutomator = smartappAutomatorRuntime;
|
|
3676
3832
|
//#endregion
|
|
3677
|
-
export { Automator, Connection, ContextElement, CustomElement, Element, InputElement, Launcher, MiniProgram, MovableViewElement, Native, Page, ScrollViewElement, SliderElement, SmartappAutomator, SwiperElement, SwitchElement, TextareaElement, Transport, decodeQrCode, extractPluginId, isPluginPath, normalizePlatform, printQrCode };
|
|
3833
|
+
export { Automator, Connection, ContextElement, CustomElement, Element, InputElement, Launcher, MiniProgram, MovableViewElement, Native, OPERATION_TIMEOUT_CODE, OperationLifecycle, Page, ScrollViewElement, SliderElement, SmartappAutomator, SwiperElement, SwitchElement, TextareaElement, Transport, acquireAutomatorPortLease, classifyOperationError, decodeQrCode, extractPluginId, isPluginPath, isRecoverableOperationError, normalizePlatform, printQrCode, readWechatLoginState };
|