@weapp-vite/miniprogram-automator 1.2.22 → 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/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
- /** sleep 的方法封装。 */
18
- function sleep$1(timeout) {
19
- return new Promise((resolve) => setTimeout(resolve, timeout));
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
- /** waitUntil 的方法封装。 */
22
- async function waitUntil(condition, timeout = 0, interval = 250) {
23
- const startTime = Date.now();
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
- if (timeout && Date.now() - startTime >= timeout) throw new Error(`Wait timed out after ${timeout} ms`);
28
- await sleep$1(interval);
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.close();
845
+ this.ws.terminate();
829
846
  }
830
847
  };
831
848
  //#endregion
@@ -839,11 +856,21 @@ const REQUEST_TIMEOUT = 3e4;
839
856
  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"]);
859
+ /** 仅页面方法调用失效的版本,元素查询仍保留原生组件作用域。 */
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
+ ]);
842
867
  /** Connection 的实现。 */
843
868
  var Connection = class Connection extends EventEmitter {
844
869
  transport;
870
+ disposed = false;
845
871
  callbacks = /* @__PURE__ */ new Map();
846
872
  useAppServicePageProtocol = false;
873
+ useAppServicePageMethod = false;
847
874
  constructor(transport) {
848
875
  super();
849
876
  this.transport = transport;
@@ -851,6 +878,7 @@ var Connection = class Connection extends EventEmitter {
851
878
  transport.on("close", this.onClose);
852
879
  }
853
880
  send(method, params = {}, options = {}) {
881
+ if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error(closeErrTip));
854
882
  const id = uuid();
855
883
  const payload = stringify({
856
884
  id,
@@ -874,23 +902,34 @@ var Connection = class Connection extends EventEmitter {
874
902
  });
875
903
  try {
876
904
  this.transport.send(payload);
877
- } catch {
905
+ } catch (cause) {
878
906
  clearTimeout(timeout);
879
907
  this.callbacks.delete(id);
880
- reject(/* @__PURE__ */ new Error(closeErrTip));
908
+ reject(new Error(closeErrTip, { cause }));
881
909
  }
882
910
  });
883
911
  }
884
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);
885
918
  this.transport.close();
886
919
  }
887
920
  /** 根据 DevTools 版本选择稳定的 Page 协议实现。 */
888
921
  configureToolInfo(info) {
889
- this.useAppServicePageProtocol = APP_SERVICE_PAGE_PROTOCOL_VERSIONS.has(String(info.version ?? ""));
922
+ const version = String(info.version ?? "");
923
+ this.useAppServicePageProtocol = APP_SERVICE_PAGE_PROTOCOL_VERSIONS.has(version);
924
+ this.useAppServicePageMethod = this.useAppServicePageProtocol || APP_SERVICE_PAGE_METHOD_VERSIONS.has(version);
890
925
  }
891
926
  get prefersAppServicePageProtocol() {
892
927
  return this.useAppServicePageProtocol;
893
928
  }
929
+ /** 页面方法调用独立选择协议,不影响节点查询及其组件作用域。 */
930
+ get prefersAppServicePageMethod() {
931
+ return this.useAppServicePageMethod;
932
+ }
894
933
  onMessage = (message) => {
895
934
  debugProtocol(`${dateFormat("yyyy-mm-dd HH:MM:ss:l")} ◀ RECV ${message}`);
896
935
  const { id, method, error, result, params } = JSON.parse(message);
@@ -915,35 +954,53 @@ var Connection = class Connection extends EventEmitter {
915
954
  }
916
955
  this.callbacks.clear();
917
956
  };
918
- static create(url, timeout = CONNECT_TIMEOUT) {
957
+ static create(url, timeout = CONNECT_TIMEOUT, signal) {
958
+ signal?.throwIfAborted();
919
959
  return new Promise((resolve, reject) => {
920
960
  const ws = new WebSocket(url);
921
961
  let settled = false;
922
- const timer = setTimeout(() => {
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) {
923
973
  if (settled) return;
924
974
  settled = true;
925
- ws.close();
926
- reject(/* @__PURE__ */ new Error(`Timed out connecting to DevTools websocket ${url} after ${timeout}ms`));
927
- }, timeout);
928
- ws.on("open", () => {
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 = () => {
929
986
  if (settled) return;
930
987
  settled = true;
931
- clearTimeout(timer);
988
+ cleanup();
932
989
  resolve(new Connection(new Transport(ws)));
933
- });
934
- ws.on("error", (error) => {
935
- if (settled) return;
936
- settled = true;
937
- clearTimeout(timer);
938
- reject(error);
939
- });
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();
940
997
  });
941
998
  }
942
999
  };
943
1000
  //#endregion
944
1001
  //#region src/headless.ts
945
1002
  async function launchHeadlessAutomator(options) {
946
- return await (await import("./launch-BNQZTcKo.mjs")).launch({ projectPath: options.projectPath });
1003
+ return await (await import("./launch-anJvfi-6.mjs")).launch({ projectPath: options.projectPath });
947
1004
  }
948
1005
  //#endregion
949
1006
  //#region src/launcher/portLease.ts
@@ -1055,7 +1112,10 @@ async function enableAutomatorViaHttp(options) {
1055
1112
  if (options.account) endpoint.searchParams.set("account", options.account);
1056
1113
  if (options.ticket) endpoint.searchParams.set("ticket", options.ticket);
1057
1114
  if (options.trustProject) endpoint.searchParams.set("trustProject", "true");
1058
- const response = await fetch(endpoint, { redirect: "follow" });
1115
+ const response = await fetch(endpoint, {
1116
+ redirect: "follow",
1117
+ signal: options.signal
1118
+ });
1059
1119
  const body = await response.text();
1060
1120
  if (!response.ok) throw new Error(`WeChat DevTools HTTP automator fallback failed with status ${response.status}`);
1061
1121
  let result;
@@ -1064,6 +1124,7 @@ async function enableAutomatorViaHttp(options) {
1064
1124
  } catch {
1065
1125
  throw new Error("WeChat DevTools HTTP automator fallback returned invalid JSON");
1066
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 });
1067
1128
  if (result && typeof result === "object" && "autoPort" in result) {
1068
1129
  const autoPort = Number(result.autoPort);
1069
1130
  if (!Number.isInteger(autoPort) || autoPort <= 0 || autoPort > 65535) throw new Error("WeChat DevTools HTTP automator fallback returned invalid autoPort");
@@ -1097,8 +1158,122 @@ function resolveWechatDevtoolsBootstrapArgs(args) {
1097
1158
  return bootstrapArgs;
1098
1159
  }
1099
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
1100
1238
  //#region package.json
1101
- var version = "1.2.22";
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
+ }
1102
1277
  //#endregion
1103
1278
  //#region src/Native.ts
1104
1279
  /** Native 的实现。 */
@@ -1149,6 +1324,25 @@ var Native = class extends EventEmitter {
1149
1324
  }
1150
1325
  };
1151
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
1152
1346
  //#region src/pageRouteFallback.ts
1153
1347
  function normalizeFallbackIdPart(value) {
1154
1348
  return encodeURIComponent(value).replace(/%/g, "_");
@@ -1420,7 +1614,7 @@ var Page = class Page {
1420
1614
  async $(selector, options = {}) {
1421
1615
  if (options.routeOnly || options.fallback !== false && this.preferAppServicePageProtocol) return await this.queryRouteElement(selector, options);
1422
1616
  try {
1423
- const element = await this.send("Page.getElement", { selector }, { timeout: options.timeout ?? PAGE_QUERY_TIMEOUT });
1617
+ const element = await this.queryPageElements("Page.getElement", selector, options);
1424
1618
  return Element.create(this.connection, {
1425
1619
  ...element,
1426
1620
  pageId: this.id
@@ -1436,7 +1630,7 @@ var Page = class Page {
1436
1630
  async $$(selector, options = {}) {
1437
1631
  if (options.routeOnly || options.fallback !== false && this.preferAppServicePageProtocol) return await this.queryRouteElements(selector, options);
1438
1632
  try {
1439
- const { elements } = await this.send("Page.getElements", { selector }, { timeout: options.timeout ?? PAGE_QUERY_TIMEOUT });
1633
+ const { elements } = await this.queryPageElements("Page.getElements", selector, options);
1440
1634
  return elements.map((element) => {
1441
1635
  return Element.create(this.connection, {
1442
1636
  ...element,
@@ -1449,6 +1643,15 @@ var Page = class Page {
1449
1643
  return await this.queryRouteElements(selector, options);
1450
1644
  }
1451
1645
  }
1646
+ async queryPageElements(method, selector, options) {
1647
+ const timeout = options.timeout ?? PAGE_QUERY_TIMEOUT;
1648
+ try {
1649
+ return await this.send(method, { selector }, { timeout });
1650
+ } catch (error) {
1651
+ if (options.fallback === false || !isRecoverablePageProtocolError(error, method)) throw error;
1652
+ return await this.send(method, { selector }, { timeout });
1653
+ }
1654
+ }
1452
1655
  async getElementByXpath(selector, options = {}) {
1453
1656
  try {
1454
1657
  const element = await this.send("Page.getElementByXpath", { selector }, { timeout: options.timeout ?? PAGE_QUERY_TIMEOUT });
@@ -2084,7 +2287,7 @@ var Page = class Page {
2084
2287
  return await this.callMethodWithOptions(method, {}, ...args);
2085
2288
  }
2086
2289
  async callMethodWithOptions(method, options = {}, ...args) {
2087
- if (options.routeOnly || options.fallback !== false && this.preferAppServicePageProtocol) return await this.callRouteMethod(method, args, options.timeout);
2290
+ if (options.routeOnly || options.fallback !== false && (this.preferAppServicePageProtocol || this.connection.prefersAppServicePageMethod)) return await this.callRouteMethod(method, args, options.timeout);
2088
2291
  try {
2089
2292
  return (await this.send("Page.callMethod", {
2090
2293
  method,
@@ -2404,7 +2607,7 @@ function isPluginNavigationUrl(value) {
2404
2607
  }
2405
2608
  function matchesRouteChange(options) {
2406
2609
  const normalizedCurrentPath = normalizeRoutePath(options.currentPath);
2407
- if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return true;
2610
+ if (!options.expectedRoute || normalizedCurrentPath === options.expectedRoute) return matchesRouteQuery(options.currentQuery, options.expectedQuery);
2408
2611
  if (!isPluginNavigationUrl(options.requestedUrl) || !isPluginPath(options.currentPath)) return false;
2409
2612
  return normalizedCurrentPath !== normalizeRoutePath(options.previousPath);
2410
2613
  }
@@ -2582,6 +2785,10 @@ var MiniProgram = class extends EventEmitter {
2582
2785
  }, sendOptions);
2583
2786
  return result;
2584
2787
  }
2788
+ /** 探测并读取 AppService JS 堆;明确不支持时返回原因,连接故障与无效响应继续抛出。 */
2789
+ async getAppServiceHeapUsage(options = {}) {
2790
+ return await readAppServiceHeapUsage((method, params, sendOptions) => this.send(method, params, sendOptions), options);
2791
+ }
2585
2792
  async pageScrollTo(scrollTop) {
2586
2793
  await this.callWxMethod("pageScrollTo", {
2587
2794
  scrollTop,
@@ -2677,7 +2884,7 @@ var MiniProgram = class extends EventEmitter {
2677
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`);
2678
2885
  }
2679
2886
  /**
2680
- * @description 等待小程序 App 域协议可用,避免返回半就绪自动化会话。
2887
+ * @description 等待原生协议返回真实页面标识和路径,避免返回半就绪自动化会话。
2681
2888
  */
2682
2889
  async waitForAppReady(timeout = APP_READY_TIMEOUT) {
2683
2890
  const startedAt = Date.now();
@@ -2685,13 +2892,15 @@ var MiniProgram = class extends EventEmitter {
2685
2892
  while (Date.now() - startedAt < timeout) {
2686
2893
  const remainingTimeout = timeout - (Date.now() - startedAt);
2687
2894
  try {
2688
- await this.send("App.captureScreenshot", {}, { timeout: Math.min(APP_READY_PROBE_TIMEOUT, remainingTimeout) });
2689
- 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");
2690
2898
  } catch (error) {
2899
+ if (!isCurrentPageProtocolTimeout(error) && !isPageMetaMissingError(error) && !isCurrentFrameTimedOutError(error)) throw error;
2691
2900
  lastError = error;
2692
- const remainingAfterProbe = timeout - (Date.now() - startedAt);
2693
- if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
2694
2901
  }
2902
+ const remainingAfterProbe = timeout - (Date.now() - startedAt);
2903
+ if (remainingAfterProbe > 0) await sleep(Math.min(APP_READY_POLL_DELAY, remainingAfterProbe));
2695
2904
  }
2696
2905
  throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Timed out waiting App domain readiness after ${timeout}ms`);
2697
2906
  }
@@ -2782,9 +2991,11 @@ var MiniProgram = class extends EventEmitter {
2782
2991
  logChangeRouteDebug(`poll method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
2783
2992
  if (matchesRouteChange({
2784
2993
  currentPath: page?.path,
2994
+ currentQuery: page?.query,
2785
2995
  previousPath: currentPage?.path,
2786
2996
  requestedUrl: url,
2787
- expectedRoute
2997
+ expectedRoute,
2998
+ expectedQuery: expectedRouteQuery
2788
2999
  })) {
2789
3000
  logChangeRouteDebug(`ready method=${method} url=${url ?? "<none>"} current=${page?.path ?? "<none>"}`);
2790
3001
  return page;
@@ -2801,9 +3012,11 @@ var MiniProgram = class extends EventEmitter {
2801
3012
  logChangeRouteDebug(`stack method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
2802
3013
  if (matchesRouteChange({
2803
3014
  currentPath: stackTop.path,
3015
+ currentQuery: stackTop.query,
2804
3016
  previousPath: currentPage?.path,
2805
3017
  requestedUrl: url,
2806
- expectedRoute
3018
+ expectedRoute,
3019
+ expectedQuery: expectedRouteQuery
2807
3020
  })) {
2808
3021
  logChangeRouteDebug(`stack-ready method=${method} url=${url ?? "<none>"} current=${stackTop.path}`);
2809
3022
  return stackTop;
@@ -3346,16 +3559,7 @@ const VERSION_CHECK_TIMEOUT = 3e4;
3346
3559
  const AUTOMATOR_LAUNCH_RETRIES = 3;
3347
3560
  const DEFAULT_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_AUTOMATOR_RUNTIME_PROVIDER";
3348
3561
  const LEGACY_RUNTIME_PROVIDER_ENV = "WEAPP_VITE_E2E_RUNTIME_PROVIDER";
3349
- const EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
3350
- 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;
3351
- const WINDOWS_BATCH_CLI_RE = /\.(?:bat|cmd)$/i;
3352
3562
  let localhostListenPatched = false;
3353
- function isExtensionContextInvalidatedError(error) {
3354
- return error instanceof Error && EXTENSION_CONTEXT_INVALIDATED_RE.test(error.message);
3355
- }
3356
- function isRetryableAutomatorPortLaunchError(error) {
3357
- return error instanceof Error && RETRYABLE_LAUNCH_PORT_RE.test(error.message);
3358
- }
3359
3563
  function retainPortLeaseUntilSessionClose(miniProgram, portLease) {
3360
3564
  let released = false;
3361
3565
  const release = async () => {
@@ -3400,25 +3604,6 @@ function patchNetListenToLoopback() {
3400
3604
  return rawListen.apply(this, args);
3401
3605
  };
3402
3606
  }
3403
- /** IConnectOptions 的类型定义。 */
3404
- function shouldUseWindowsCommandShell(cliPath) {
3405
- return isWindows && WINDOWS_BATCH_CLI_RE.test(cliPath);
3406
- }
3407
- function escapeWindowsCmdArg(arg) {
3408
- const escaped = arg.replace(/"/g, "\"\"").replace(/%/g, "%%");
3409
- return /[\s"&<>^|()]/.test(arg) ? `"${escaped}"` : escaped;
3410
- }
3411
- function resolveWindowsBatchSpawn(cliPath, args) {
3412
- return {
3413
- file: process.env.ComSpec || "cmd.exe",
3414
- args: [
3415
- "/d",
3416
- "/s",
3417
- "/c",
3418
- `"${[cliPath, ...args].map(escapeWindowsCmdArg).join(" ")}"`
3419
- ]
3420
- };
3421
- }
3422
3607
  function resolveRuntimeProvider(options) {
3423
3608
  return options.runtimeProvider || process.env[DEFAULT_RUNTIME_PROVIDER_ENV] || process.env[LEGACY_RUNTIME_PROVIDER_ENV] || "devtools";
3424
3609
  }
@@ -3431,88 +3616,62 @@ var Launcher = class {
3431
3616
  return await launchHeadlessAutomator({ projectPath: options.projectPath });
3432
3617
  }
3433
3618
  patchNetListenToLoopback();
3434
- if (options.port) return await this.launchWechatDevtools(options);
3435
- let lastError = null;
3436
- for (let attempt = 1; attempt <= AUTOMATOR_LAUNCH_RETRIES; attempt += 1) try {
3437
- return await this.launchWechatDevtools(options);
3438
- } catch (error) {
3439
- lastError = error;
3440
- if (!isRetryableAutomatorPortLaunchError(error) || attempt === AUTOMATOR_LAUNCH_RETRIES) throw error;
3441
- }
3442
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
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
+ });
3443
3632
  }
3444
- async launchWechatDevtools(options) {
3445
- const { cliPath = await this.resolveCliPath(), timeout = DEFAULT_TIMEOUT, projectConfig = {}, ticket = "", cwd = "", account = "", trustProject = false } = options;
3446
- let { args = [], projectPath } = options;
3447
- 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");
3448
3644
  let releasePortLeaseOnExit = true;
3645
+ let releaseChild = async () => {};
3449
3646
  try {
3450
3647
  const port = portLease.port;
3451
3648
  if (!cliPath) throw new Error("Wechat web devTools not found, please specify cliPath option");
3452
3649
  if (isWindows && endWith(cliPath, ".exe")) throw new Error("cliPath is not correct, it's usually named as 'cli' or 'cli.bat'");
3453
3650
  if (!projectPath) throw new Error("projectPath is not provided");
3454
3651
  const resolvedProjectPath = isRelative(projectPath) ? path.resolve(projectPath) : projectPath;
3455
- if (isRelative(projectPath)) projectPath = resolvedProjectPath;
3456
- if (!await import("node:fs/promises").then((fs) => fs.access(resolvedProjectPath).then(() => true).catch(() => false))) throw new Error(`Project path ${resolvedProjectPath} doesn't exist`);
3457
- if (!isEmpty(projectConfig)) await this.extendProjectConfig(projectConfig, resolvedProjectPath);
3458
- let processError = null;
3459
- let processExitCode = null;
3460
- let processSignal = null;
3461
- 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();
3462
3655
  let httpFallbackAttempted = false;
3463
3656
  let httpFallbackError = null;
3464
3657
  let targetPort = port;
3465
- const cliOutput = [];
3466
- args = resolveWechatDevtoolsBootstrapArgs(args);
3467
- try {
3468
- const spawnTarget = shouldUseWindowsCommandShell(cliPath) ? resolveWindowsBatchSpawn(cliPath, args) : {
3469
- file: cliPath,
3470
- args
3471
- };
3472
- const child = spawn(spawnTarget.file, spawnTarget.args, {
3473
- stdio: [
3474
- "ignore",
3475
- "pipe",
3476
- "pipe"
3477
- ],
3478
- cwd: cwd || void 0,
3479
- ...shouldUseWindowsCommandShell(cliPath) ? {
3480
- windowsHide: true,
3481
- windowsVerbatimArguments: true
3482
- } : {}
3483
- });
3484
- child.on("error", (error) => {
3485
- processError = error;
3486
- });
3487
- child.stdout?.on("data", (chunk) => {
3488
- cliOutput.push(String(chunk));
3489
- });
3490
- child.stderr?.on("data", (chunk) => {
3491
- cliOutput.push(String(chunk));
3492
- });
3493
- child.on("exit", (code, signal) => {
3494
- processExitCode = code;
3495
- processSignal = signal;
3496
- if (code !== 0 || signal) processError = /* @__PURE__ */ new Error(`DevTools cli exited unexpectedly with code ${code ?? "null"}${signal ? ` and signal ${signal}` : ""}`);
3497
- else successfulCliExitSettled = true;
3498
- });
3499
- child.unref();
3500
- } catch (error) {
3501
- processError = error;
3502
- }
3658
+ const cli = spawnWechatCli(cliPath, args, cwd, scope);
3659
+ releaseChild = cli.release;
3503
3660
  let miniProgram = null;
3504
3661
  let lastConnectError = null;
3505
- const readinessStartedAt = Date.now();
3506
- const resolveRemainingTimeout = () => Math.max(0, timeout - (Date.now() - readinessStartedAt));
3507
- await waitUntil(async () => {
3662
+ const resolveRemainingTimeout = () => scope.remainingMs();
3663
+ await scope.step(() => waitUntil(async () => {
3664
+ scope.throwIfAborted();
3508
3665
  try {
3509
- if (processError) return true;
3510
- if (successfulCliExitSettled && !httpFallbackAttempted) {
3511
- const servicePort = extractWechatDevtoolsServicePort(cliOutput.join("\n"));
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);
3512
3670
  if (servicePort) {
3513
3671
  httpFallbackAttempted = true;
3514
3672
  try {
3515
3673
  targetPort = await enableAutomatorViaHttp({
3674
+ signal: scope.signal,
3516
3675
  account,
3517
3676
  autoPort: port,
3518
3677
  projectPath: resolvedProjectPath,
@@ -3528,43 +3687,59 @@ var Launcher = class {
3528
3687
  }
3529
3688
  const connectTimeout = resolveRemainingTimeout();
3530
3689
  if (connectTimeout <= 0) return false;
3531
- const candidate = await this.connectTool({
3690
+ const candidate = await scope.step(() => this.connectTool({
3691
+ signal: scope.signal,
3532
3692
  timeout: Math.min(3e3, connectTimeout),
3533
3693
  wsEndpoint: `ws://127.0.0.1:${targetPort}`
3694
+ }), {
3695
+ stage: "websocket",
3696
+ disposeLate: (program) => program.disconnect()
3534
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");
3535
3706
  try {
3536
3707
  const checkVersionTimeout = resolveRemainingTimeout();
3537
3708
  if (checkVersionTimeout <= 0) {
3538
- candidate.disconnect();
3709
+ releaseCandidate();
3539
3710
  return false;
3540
3711
  }
3541
- await candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout));
3712
+ await scope.step(() => candidate.checkVersion(Math.min(VERSION_CHECK_TIMEOUT, checkVersionTimeout)), { stage: "version" });
3542
3713
  if (typeof candidate.waitForAppReady === "function") {
3543
3714
  const appReadyTimeout = resolveRemainingTimeout();
3544
3715
  if (appReadyTimeout <= 0) {
3545
- candidate.disconnect();
3716
+ releaseCandidate();
3546
3717
  return false;
3547
3718
  }
3548
- await candidate.waitForAppReady(appReadyTimeout);
3719
+ await scope.step(() => candidate.waitForAppReady(appReadyTimeout), { stage: "app-ready" });
3549
3720
  }
3550
3721
  } catch (error) {
3551
- candidate.disconnect();
3722
+ disownCandidate();
3723
+ releaseCandidate();
3552
3724
  lastConnectError = error;
3553
- if (isExtensionContextInvalidatedError(error)) return false;
3725
+ if (isRecoverableOperationError(error)) return false;
3554
3726
  throw error;
3555
3727
  }
3556
3728
  miniProgram = candidate;
3557
3729
  return true;
3558
3730
  } catch (error) {
3559
3731
  lastConnectError = error;
3732
+ scope.recordFailure(error);
3733
+ scope.throwIfAborted();
3734
+ if (!isRecoverableOperationError(error)) throw error;
3560
3735
  return false;
3561
3736
  }
3562
- }, timeout, 1e3);
3737
+ }, scope.remainingMs(), 1e3, scope.signal), { stage: "readiness" });
3563
3738
  if (!miniProgram) {
3564
3739
  if (httpFallbackError) throw httpFallbackError;
3565
- if (processError) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified");
3740
+ if (cli.error) throw new Error("Failed to launch wechat web devTools, please make sure cliPath is correctly specified", { cause: cli.error });
3566
3741
  if (lastConnectError) throw lastConnectError;
3567
- if (processExitCode !== null || processSignal) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
3742
+ if (cli.exited) throw new Error("Failed to launch wechat web devTools, please make sure http port is open");
3568
3743
  throw new Error("Failed connecting to devtools websocket endpoint");
3569
3744
  }
3570
3745
  const resolvedMiniProgram = miniProgram;
@@ -3574,33 +3749,32 @@ var Launcher = class {
3574
3749
  wsEndpoint: `ws://127.0.0.1:${targetPort}`
3575
3750
  });
3576
3751
  releasePortLeaseOnExit = !retainPortLeaseUntilSessionClose(resolvedMiniProgram, portLease);
3577
- await sleep$1(5e3);
3752
+ disownLease();
3578
3753
  return resolvedMiniProgram;
3579
3754
  } finally {
3580
- if (releasePortLeaseOnExit) await portLease.release();
3755
+ await releaseChild();
3756
+ if (releasePortLeaseOnExit) {
3757
+ disownLease();
3758
+ await portLease.release();
3759
+ }
3581
3760
  }
3582
3761
  }
3583
3762
  async connect(options) {
3584
3763
  if (normalizePlatform(options.platform) === "swan") return await new SwanLauncher().connect(options);
3585
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
3586
- const deadlineAt = performance.now() + timeout;
3587
- const miniProgram = await this.connectTool({
3588
- ...options,
3589
- timeout
3590
- });
3591
- try {
3592
- const remaining = Math.ceil(deadlineAt - performance.now());
3593
- if (remaining <= 0) throw new Error(`Timed out connecting to automator after ${timeout}ms`);
3594
- await miniProgram.checkVersion(remaining);
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" });
3595
3776
  return miniProgram;
3596
- } catch (error) {
3597
- try {
3598
- miniProgram.disconnect();
3599
- } catch (cleanupError) {
3600
- throw new AggregateError([error, cleanupError], "Automator connection and cleanup both failed", { cause: error });
3601
- }
3602
- throw error;
3603
- }
3777
+ });
3604
3778
  }
3605
3779
  async extendProjectConfig(projectConfig, projectPath) {
3606
3780
  const projectConfigPath = path.resolve(projectPath, "project.config.json");
@@ -3612,9 +3786,9 @@ var Launcher = class {
3612
3786
  }
3613
3787
  async connectTool(options) {
3614
3788
  try {
3615
- return new MiniProgram(await Connection.create(options.wsEndpoint, options.timeout));
3616
- } catch {
3617
- 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 });
3618
3792
  }
3619
3793
  }
3620
3794
  async resolveCliPath() {
@@ -3656,4 +3830,4 @@ var Automator = class {
3656
3830
  */
3657
3831
  const SmartappAutomator = smartappAutomatorRuntime;
3658
3832
  //#endregion
3659
- 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 };