@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/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
@@ -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(["2.02.2608070", "2.02.2609231"]);
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(/* @__PURE__ */ new Error(closeErrTip));
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
- 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) {
932
973
  if (settled) return;
933
974
  settled = true;
934
- ws.close();
935
- reject(/* @__PURE__ */ new Error(`Timed out connecting to DevTools websocket ${url} after ${timeout}ms`));
936
- }, timeout);
937
- 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 = () => {
938
986
  if (settled) return;
939
987
  settled = true;
940
- clearTimeout(timer);
988
+ cleanup();
941
989
  resolve(new Connection(new Transport(ws)));
942
- });
943
- ws.on("error", (error) => {
944
- if (settled) return;
945
- settled = true;
946
- clearTimeout(timer);
947
- reject(error);
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-CSjvON-f.mjs")).launch({ projectPath: options.projectPath });
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, { redirect: "follow" });
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.2.23";
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 true;
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 等待小程序 App 域协议可用,避免返回半就绪自动化会话。
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.captureScreenshot", {}, { timeout: Math.min(APP_READY_PROBE_TIMEOUT, remainingTimeout) });
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
- if (options.port) return await this.launchWechatDevtools(options);
3453
- let lastError = null;
3454
- for (let attempt = 1; attempt <= AUTOMATOR_LAUNCH_RETRIES; attempt += 1) try {
3455
- return await this.launchWechatDevtools(options);
3456
- } catch (error) {
3457
- lastError = error;
3458
- if (!isRetryableAutomatorPortLaunchError(error) || attempt === AUTOMATOR_LAUNCH_RETRIES) throw error;
3459
- }
3460
- 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
+ });
3461
3632
  }
3462
- async launchWechatDevtools(options) {
3463
- const { cliPath = await this.resolveCliPath(), timeout = DEFAULT_TIMEOUT, projectConfig = {}, ticket = "", cwd = "", account = "", trustProject = false } = options;
3464
- let { args = [], projectPath } = options;
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 (isRelative(projectPath)) projectPath = resolvedProjectPath;
3474
- if (!await import("node:fs/promises").then((fs) => fs.access(resolvedProjectPath).then(() => true).catch(() => false))) throw new Error(`Project path ${resolvedProjectPath} doesn't exist`);
3475
- if (!isEmpty(projectConfig)) await this.extendProjectConfig(projectConfig, resolvedProjectPath);
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 cliOutput = [];
3484
- args = resolveWechatDevtoolsBootstrapArgs(args);
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 readinessStartedAt = Date.now();
3524
- const resolveRemainingTimeout = () => Math.max(0, timeout - (Date.now() - readinessStartedAt));
3525
- await waitUntil(async () => {
3662
+ const resolveRemainingTimeout = () => scope.remainingMs();
3663
+ await scope.step(() => waitUntil(async () => {
3664
+ scope.throwIfAborted();
3526
3665
  try {
3527
- if (processError) return true;
3528
- if (successfulCliExitSettled && !httpFallbackAttempted) {
3529
- 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);
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
- candidate.disconnect();
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
- candidate.disconnect();
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
- candidate.disconnect();
3722
+ disownCandidate();
3723
+ releaseCandidate();
3570
3724
  lastConnectError = error;
3571
- if (isExtensionContextInvalidatedError(error)) return false;
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
- }, timeout, 1e3);
3737
+ }, scope.remainingMs(), 1e3, scope.signal), { stage: "readiness" });
3581
3738
  if (!miniProgram) {
3582
3739
  if (httpFallbackError) throw httpFallbackError;
3583
- 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 });
3584
3741
  if (lastConnectError) throw lastConnectError;
3585
- 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");
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
- await sleep$1(5e3);
3752
+ disownLease();
3596
3753
  return resolvedMiniProgram;
3597
3754
  } finally {
3598
- if (releasePortLeaseOnExit) await portLease.release();
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
- const timeout = options.timeout ?? DEFAULT_TIMEOUT;
3604
- const deadlineAt = performance.now() + timeout;
3605
- const miniProgram = await this.connectTool({
3606
- ...options,
3607
- timeout
3608
- });
3609
- try {
3610
- const remaining = Math.ceil(deadlineAt - performance.now());
3611
- if (remaining <= 0) throw new Error(`Timed out connecting to automator after ${timeout}ms`);
3612
- 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" });
3613
3776
  return miniProgram;
3614
- } catch (error) {
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 };