weapp-ide-cli 6.1.9 → 6.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.
@@ -1,1362 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
- import { createHash } from "node:crypto";
3
- import fs from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { Launcher } from "@weapp-vite/miniprogram-automator";
7
- import { fs as fs$1 } from "@weapp-core/shared/fs";
8
- import process from "node:process";
9
- import path$1 from "pathe";
10
- import logger, { colors } from "@weapp-core/logger";
11
- import { acquireSharedMiniProgram, closeSharedMiniProgram, getSharedMiniProgramSessionCount, releaseSharedMiniProgram, withMiniProgram } from "@weapp-vite/devtools-runtime";
12
- import { emitKeypressEvents } from "node:readline";
13
- //#region src/utils/path.ts
14
- /**
15
- * @description 解析为绝对路径(基于当前工作目录)
16
- */
17
- function resolvePath(filePath) {
18
- if (path$1.isAbsolute(filePath)) return filePath;
19
- return path$1.resolve(process.cwd(), filePath);
20
- }
21
- //#endregion
22
- //#region src/config/paths.ts
23
- const homedir = os.homedir();
24
- /**
25
- * @description 默认自定义配置目录
26
- */
27
- const defaultCustomConfigDirPath = path$1.join(homedir, ".weapp-ide-cli");
28
- /**
29
- * @description 默认自定义配置文件路径
30
- */
31
- const defaultCustomConfigFilePath = path$1.join(defaultCustomConfigDirPath, "config.json");
32
- //#endregion
33
- //#region src/config/custom.ts
34
- const JSON_OPTIONS = {
35
- encoding: "utf8",
36
- spaces: 2
37
- };
38
- async function readCustomConfig() {
39
- if (!await fs$1.pathExists(defaultCustomConfigFilePath)) return {};
40
- try {
41
- const config = await fs$1.readJSON(defaultCustomConfigFilePath);
42
- if (!config || typeof config !== "object") return {};
43
- const candidate = config;
44
- const next = {};
45
- if (typeof candidate.cliPath === "string" && candidate.cliPath.trim()) next.cliPath = candidate.cliPath.trim();
46
- if (candidate.locale === "zh" || candidate.locale === "en") next.locale = candidate.locale;
47
- if (typeof candidate.autoBootstrapDevtools === "boolean") next.autoBootstrapDevtools = candidate.autoBootstrapDevtools;
48
- if (typeof candidate.autoTrustProject === "boolean") next.autoTrustProject = candidate.autoTrustProject;
49
- return next;
50
- } catch {
51
- return {};
52
- }
53
- }
54
- async function writeCustomConfig(patch, options = {}) {
55
- const nextConfig = {
56
- ...options.replace ? {} : await readCustomConfig(),
57
- ...patch
58
- };
59
- await fs$1.ensureDir(defaultCustomConfigDirPath);
60
- await fs$1.writeJSON(defaultCustomConfigFilePath, nextConfig, JSON_OPTIONS);
61
- }
62
- /**
63
- * @description 写入自定义 CLI 路径配置
64
- */
65
- async function createCustomConfig(params) {
66
- const trimmedCliPath = params.cliPath.trim();
67
- if (!trimmedCliPath) throw new Error("cliPath cannot be empty");
68
- const normalizedCliPath = resolvePath(trimmedCliPath);
69
- await writeCustomConfig({ cliPath: normalizedCliPath });
70
- return normalizedCliPath;
71
- }
72
- /**
73
- * @description 写入语言配置(zh / en)。
74
- */
75
- async function createLocaleConfig(locale) {
76
- await writeCustomConfig({ locale });
77
- return locale;
78
- }
79
- /**
80
- * @description 写入开发者工具自动预热配置。
81
- */
82
- async function createAutoBootstrapDevtoolsConfig(value) {
83
- await writeCustomConfig({ autoBootstrapDevtools: value });
84
- return value;
85
- }
86
- /**
87
- * @description 写入项目自动信任配置。
88
- */
89
- async function createAutoTrustProjectConfig(value) {
90
- await writeCustomConfig({ autoTrustProject: value });
91
- return value;
92
- }
93
- /**
94
- * @description 删除指定配置项。
95
- */
96
- async function removeCustomConfigKey(key) {
97
- const currentConfig = await readCustomConfig();
98
- if (!(key in currentConfig)) return;
99
- const nextConfig = { ...currentConfig };
100
- delete nextConfig[key];
101
- await writeCustomConfig(nextConfig, { replace: true });
102
- }
103
- /**
104
- * @description 覆盖写入配置内容(会替换原内容)。
105
- */
106
- async function overwriteCustomConfig(config) {
107
- const nextConfig = {};
108
- if (typeof config.cliPath === "string" && config.cliPath.trim()) nextConfig.cliPath = resolvePath(config.cliPath.trim());
109
- if (config.locale === "zh" || config.locale === "en") nextConfig.locale = config.locale;
110
- if (typeof config.autoBootstrapDevtools === "boolean") nextConfig.autoBootstrapDevtools = config.autoBootstrapDevtools;
111
- if (typeof config.autoTrustProject === "boolean") nextConfig.autoTrustProject = config.autoTrustProject;
112
- await writeCustomConfig(nextConfig, { replace: true });
113
- }
114
- //#endregion
115
- //#region src/cli/automatorProject.ts
116
- const WRAPPER_ROOT = path.join(os.tmpdir(), "weapp-ide-cli-automator-projects");
117
- function normalizeProjectRelativeRoot(rawRoot) {
118
- if (typeof rawRoot !== "string") return;
119
- const normalized = rawRoot.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
120
- if (!normalized || normalized === ".") return;
121
- if (normalized.split("/").includes("..")) return;
122
- return normalized;
123
- }
124
- async function pathExists(filePath) {
125
- try {
126
- await fs.access(filePath);
127
- return true;
128
- } catch {
129
- return false;
130
- }
131
- }
132
- async function readJsonObject$1(filePath) {
133
- try {
134
- const raw = await fs.readFile(filePath, "utf8");
135
- const value = JSON.parse(raw);
136
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
137
- } catch {
138
- return;
139
- }
140
- }
141
- async function writeJsonObject$1(filePath, value) {
142
- await fs.mkdir(path.dirname(filePath), { recursive: true });
143
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
144
- }
145
- async function copyProjectRoot(sourceProjectPath, wrapperProjectPath, relativeRoot) {
146
- const sourcePath = path.join(sourceProjectPath, relativeRoot);
147
- if (!await pathExists(sourcePath)) return;
148
- await fs.cp(sourcePath, path.join(wrapperProjectPath, relativeRoot), {
149
- dereference: true,
150
- force: true,
151
- recursive: true
152
- });
153
- }
154
- function createWrapperProjectConfig(source, patch) {
155
- const { miniprogramRoot: _miniprogramRoot, qcloudRoot: _qcloudRoot, srcMiniprogramRoot: _srcMiniprogramRoot, ...rest } = source;
156
- const sourceSetting = source.setting && typeof source.setting === "object" ? source.setting : {};
157
- const patchSetting = patch.setting && typeof patch.setting === "object" ? patch.setting : {};
158
- return {
159
- ...rest,
160
- ...patch,
161
- setting: {
162
- ...sourceSetting,
163
- ...patchSetting,
164
- packNpmManually: false,
165
- packNpmRelationList: []
166
- }
167
- };
168
- }
169
- async function ensureWrapperAppConfig(wrapperProjectPath) {
170
- const appConfigPath = path.join(wrapperProjectPath, "app.json");
171
- const appConfig = await readJsonObject$1(appConfigPath);
172
- if (!appConfig) return;
173
- await writeJsonObject$1(appConfigPath, {
174
- ...appConfig,
175
- subPackages: Array.isArray(appConfig.subPackages) ? appConfig.subPackages : []
176
- });
177
- }
178
- /**
179
- * @description 为 DevTools automator 准备稳定的小程序根目录,避免打开瞬间跨 miniprogramRoot 读取 app.json 抖动。
180
- */
181
- async function resolveAutomatorProjectPath(projectPath) {
182
- const sourceProjectPath = path.resolve(projectPath);
183
- const projectConfig = await readJsonObject$1(path.join(sourceProjectPath, "project.config.json"));
184
- const miniprogramRoot = normalizeProjectRelativeRoot(projectConfig?.miniprogramRoot);
185
- if (!miniprogramRoot) return {
186
- projectPath: sourceProjectPath,
187
- sourceProjectPath
188
- };
189
- const sourceProjectConfig = projectConfig ?? {};
190
- const distRoot = path.join(sourceProjectPath, miniprogramRoot);
191
- if (!await pathExists(path.join(distRoot, "app.json"))) return {
192
- projectPath: sourceProjectPath,
193
- sourceProjectPath
194
- };
195
- const wrapperHash = createHash("sha1").update(sourceProjectPath).update("\0").update(path.resolve(distRoot)).digest("hex").slice(0, 16);
196
- const wrapperProjectPath = path.join(WRAPPER_ROOT, wrapperHash);
197
- await fs.rm(wrapperProjectPath, {
198
- recursive: true,
199
- force: true
200
- });
201
- await fs.mkdir(wrapperProjectPath, { recursive: true });
202
- await fs.cp(distRoot, wrapperProjectPath, {
203
- dereference: true,
204
- force: true,
205
- recursive: true
206
- });
207
- const pluginRoot = normalizeProjectRelativeRoot(sourceProjectConfig.pluginRoot);
208
- if (pluginRoot) await copyProjectRoot(sourceProjectPath, wrapperProjectPath, pluginRoot);
209
- const rootPatch = {
210
- compileType: sourceProjectConfig.compileType === "plugin" ? "plugin" : "miniprogram",
211
- miniprogramRoot: "./",
212
- srcMiniprogramRoot: "./"
213
- };
214
- const projectPrivateConfig = await readJsonObject$1(path.join(sourceProjectPath, "project.private.config.json")) ?? {};
215
- await writeJsonObject$1(path.join(wrapperProjectPath, "project.config.json"), createWrapperProjectConfig(sourceProjectConfig, {
216
- ...projectPrivateConfig,
217
- ...rootPatch
218
- }));
219
- await fs.rm(path.join(wrapperProjectPath, "project.private.config.json"), { force: true });
220
- await ensureWrapperAppConfig(wrapperProjectPath);
221
- return {
222
- cleanup: async () => {
223
- await fs.rm(wrapperProjectPath, {
224
- recursive: true,
225
- force: true
226
- });
227
- },
228
- projectPath: wrapperProjectPath,
229
- sourceProjectPath
230
- };
231
- }
232
- //#endregion
233
- //#region src/logger.ts
234
- var logger_default = logger;
235
- //#endregion
236
- //#region src/runtime/platform.ts
237
- /**
238
- * @description 官方微信开发者工具只支持 Windows、macOS,Linux 只有社区版。
239
- * https://github.com/msojocs/wechat-web-devtools-linux
240
- */
241
- const SupportedPlatformsMap = {
242
- Windows_NT: "Windows_NT",
243
- Darwin: "Darwin",
244
- Linux: "Linux"
245
- };
246
- /**
247
- * @description 判断当前系统是否支持微信开发者工具。
248
- */
249
- function isOperatingSystemSupported(osName = os.type()) {
250
- return osName === SupportedPlatformsMap.Windows_NT || osName === SupportedPlatformsMap.Darwin || osName === SupportedPlatformsMap.Linux;
251
- }
252
- /**
253
- * @description 当前系统名称。
254
- */
255
- const operatingSystemName = os.type();
256
- async function findFirstExistingPath(candidates) {
257
- for (const candidate of candidates) {
258
- if (!candidate) continue;
259
- if (await fs$1.pathExists(candidate)) return candidate;
260
- }
261
- }
262
- async function getFirstBinaryPath(command) {
263
- const pathDirs = (process.env.PATH || "").split(path$1.delimiter);
264
- for (const dir of pathDirs) {
265
- const fullPath = path$1.join(dir, command);
266
- try {
267
- await fs$1.access(fullPath, fs$1.constants.X_OK);
268
- return fullPath;
269
- } catch {
270
- continue;
271
- }
272
- }
273
- }
274
- function createLinuxCliResolver() {
275
- let resolvedPath;
276
- let attempted = false;
277
- let pending = null;
278
- return async () => {
279
- if (attempted) return resolvedPath;
280
- if (!pending) pending = (async () => {
281
- try {
282
- const envPath = await getFirstBinaryPath("wechat-devtools-cli");
283
- if (envPath) resolvedPath = envPath;
284
- } catch (error) {
285
- const reason = error instanceof Error ? error.message : String(error);
286
- logger_default.warn(`获取 Linux wechat-devtools-cli 路径失败:${reason}`);
287
- } finally {
288
- attempted = true;
289
- }
290
- return resolvedPath;
291
- })();
292
- return pending;
293
- };
294
- }
295
- function createWindowsCliResolver() {
296
- let resolvedPath;
297
- let attempted = false;
298
- let pending = null;
299
- return async () => {
300
- if (attempted) return resolvedPath;
301
- if (!pending) pending = (async () => {
302
- const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
303
- const programFiles = process.env.ProgramFiles || "C:\\Program Files";
304
- const localAppData = process.env.LOCALAPPDATA || path$1.join(os.homedir(), "AppData", "Local");
305
- const candidates = [
306
- path$1.join(programFilesX86, "Tencent", "微信web开发者工具", "cli.bat"),
307
- path$1.join(programFiles, "Tencent", "微信web开发者工具", "cli.bat"),
308
- path$1.join(localAppData, "Programs", "微信开发者工具", "cli.bat"),
309
- path$1.join(localAppData, "Tencent", "微信web开发者工具", "cli.bat")
310
- ];
311
- try {
312
- resolvedPath = await findFirstExistingPath(candidates);
313
- } catch (error) {
314
- const reason = error instanceof Error ? error.message : String(error);
315
- logger_default.warn(`获取 Windows 微信开发者工具 CLI 路径失败:${reason}`);
316
- } finally {
317
- attempted = true;
318
- }
319
- return resolvedPath;
320
- })();
321
- return pending;
322
- };
323
- }
324
- const linuxCliResolver = createLinuxCliResolver();
325
- const windowsCliResolver = createWindowsCliResolver();
326
- const DARWIN_DEFAULT_CLI = "/Applications/wechatwebdevtools.app/Contents/MacOS/cli";
327
- const cliPathResolvers = {
328
- [SupportedPlatformsMap.Windows_NT]: windowsCliResolver,
329
- [SupportedPlatformsMap.Darwin]: async () => DARWIN_DEFAULT_CLI,
330
- [SupportedPlatformsMap.Linux]: linuxCliResolver
331
- };
332
- /**
333
- * @description 获取默认 CLI 路径(按系统)。
334
- */
335
- async function getDefaultCliPath(targetOs = operatingSystemName) {
336
- if (!isOperatingSystemSupported(targetOs)) return;
337
- const resolver = cliPathResolvers[targetOs];
338
- return await resolver();
339
- }
340
- //#endregion
341
- //#region src/config/resolver.ts
342
- function isCustomConfigJson(value) {
343
- return typeof value === "object" && value !== null;
344
- }
345
- /**
346
- * @description 解析开发者工具自动预热相关配置的最终生效值。
347
- */
348
- function resolveDevtoolsAutomationDefaults(config) {
349
- return {
350
- autoBootstrapDevtools: config.autoBootstrapDevtools ?? true,
351
- autoTrustProject: config.autoTrustProject ?? false
352
- };
353
- }
354
- /**
355
- * @description 读取并解析 CLI 配置(自定义优先)
356
- */
357
- async function getConfig() {
358
- if (await fs$1.pathExists(defaultCustomConfigFilePath)) try {
359
- const rawConfig = await fs$1.readJSON(defaultCustomConfigFilePath);
360
- const config = isCustomConfigJson(rawConfig) ? rawConfig : {};
361
- const cliPath = typeof config.cliPath === "string" ? config.cliPath.trim() : "";
362
- const locale = config.locale === "zh" || config.locale === "en" ? config.locale : void 0;
363
- const autoBootstrapDevtools = typeof config.autoBootstrapDevtools === "boolean" ? config.autoBootstrapDevtools : void 0;
364
- const autoTrustProject = typeof config.autoTrustProject === "boolean" ? config.autoTrustProject : void 0;
365
- if (cliPath) {
366
- logger_default.info(`全局配置文件路径:${colors.green(defaultCustomConfigFilePath)}`);
367
- logger_default.info(`自定义 CLI 路径:${colors.green(cliPath)}`);
368
- return {
369
- cliPath,
370
- locale,
371
- autoBootstrapDevtools,
372
- autoTrustProject,
373
- source: "custom"
374
- };
375
- }
376
- logger_default.warn("自定义配置文件缺少有效的 CLI 路径,将尝试使用默认路径。");
377
- } catch (error) {
378
- const reason = error instanceof Error ? error.message : String(error);
379
- logger_default.warn(`解析自定义配置失败,将尝试使用默认路径。原因:${reason}`);
380
- }
381
- const fallbackPath = await getDefaultCliPath();
382
- if (fallbackPath) return {
383
- cliPath: fallbackPath,
384
- locale: void 0,
385
- autoBootstrapDevtools: void 0,
386
- autoTrustProject: void 0,
387
- source: "default"
388
- };
389
- return {
390
- cliPath: "",
391
- locale: void 0,
392
- autoBootstrapDevtools: void 0,
393
- autoTrustProject: void 0,
394
- source: "missing"
395
- };
396
- }
397
- /**
398
- * @description 获取用户配置的语言偏好。
399
- */
400
- async function getConfiguredLocale() {
401
- return (await readCustomConfig()).locale;
402
- }
403
- //#endregion
404
- //#region src/cli/resolver.ts
405
- /**
406
- * @description 解析 CLI 路径并校验可用性
407
- */
408
- async function resolveCliPath() {
409
- const config = await getConfig();
410
- if (!config.cliPath) return {
411
- cliPath: null,
412
- source: config.source
413
- };
414
- return {
415
- cliPath: await fs$1.pathExists(config.cliPath) ? config.cliPath : null,
416
- source: config.source
417
- };
418
- }
419
- //#endregion
420
- //#region src/cli/wechatDevtoolsSettings.ts
421
- function createStorageHash(key) {
422
- return createHash("md5").update(key).digest("hex");
423
- }
424
- const SETTINGS_STORAGE_HASH = createStorageHash("reduxPersist:settings");
425
- const SETTINGS_STORAGE_FILE_NAMES = [`localstorage_${SETTINGS_STORAGE_HASH}.json`, `ls_${SETTINGS_STORAGE_HASH}.json`];
426
- function resolveWechatDevtoolsBaseDir(homeDir, platform, localAppDataDir) {
427
- if (platform === "darwin") return path.join(homeDir, "Library", "Application Support", "微信开发者工具");
428
- if (platform === "win32") return path.join(localAppDataDir, "微信开发者工具", "User Data");
429
- }
430
- function isRecord(value) {
431
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
432
- }
433
- function normalizePort(value) {
434
- if (typeof value !== "number" || !Number.isInteger(value)) return;
435
- if (value <= 0 || value > 65535) return;
436
- return value;
437
- }
438
- function normalizeWechatDevtoolsSecuritySettings(value) {
439
- if (!isRecord(value)) return;
440
- const normalized = {};
441
- if (typeof value.enableServicePort === "boolean") normalized.enableServicePort = value.enableServicePort;
442
- const port = normalizePort(value.port);
443
- if (port !== void 0) normalized.port = port;
444
- if (typeof value.allowGetTicket === "boolean") normalized.allowGetTicket = value.allowGetTicket;
445
- if (typeof value.trustWhenAuto === "boolean") normalized.trustWhenAuto = value.trustWhenAuto;
446
- return Object.keys(normalized).length > 0 ? normalized : void 0;
447
- }
448
- function shouldPreferServicePortCandidate(current, next, currentUpdatedAt, nextUpdatedAt) {
449
- if (current.enabled === void 0 && current.port === void 0) return true;
450
- if (next.enabled === true && current.enabled !== true) return true;
451
- if (next.enabled === current.enabled && next.port !== void 0 && current.port === void 0) return true;
452
- return next.enabled === current.enabled && next.port !== void 0 === (current.port !== void 0) && nextUpdatedAt > currentUpdatedAt;
453
- }
454
- function createResolvedWechatDevtoolsContext(options = {}) {
455
- const platform = options.platform ?? process.platform;
456
- const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? os.homedir();
457
- if (!homeDir) return;
458
- const localAppDataDir = options.localAppDataDir ?? process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
459
- const baseDir = resolveWechatDevtoolsBaseDir(homeDir, platform, localAppDataDir);
460
- if (!baseDir) return;
461
- return {
462
- baseDir,
463
- homeDir,
464
- localAppDataDir,
465
- platform
466
- };
467
- }
468
- async function readJsonObject(filePath) {
469
- try {
470
- const raw = await fs.readFile(filePath, "utf8");
471
- const parsed = JSON.parse(raw);
472
- if (!isRecord(parsed)) return {};
473
- return parsed;
474
- } catch (error) {
475
- if (error.code === "ENOENT") return {};
476
- if (error instanceof SyntaxError) return {};
477
- throw error;
478
- }
479
- }
480
- async function writeJsonObject(filePath, value) {
481
- await fs.mkdir(path.dirname(filePath), { recursive: true });
482
- await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
483
- }
484
- async function resolveWechatDevtoolsInstanceDirs(baseDir) {
485
- try {
486
- const entries = await fs.readdir(baseDir, { withFileTypes: true });
487
- const instanceDirs = [];
488
- const seenDirs = /* @__PURE__ */ new Set();
489
- const appendInstanceDir = async (instanceDir) => {
490
- if (seenDirs.has(instanceDir)) return;
491
- try {
492
- if ((await fs.stat(path.join(instanceDir, "WeappLocalData"))).isDirectory()) {
493
- instanceDirs.push(instanceDir);
494
- seenDirs.add(instanceDir);
495
- }
496
- } catch {}
497
- };
498
- await appendInstanceDir(baseDir);
499
- for (const entry of entries) {
500
- if (!entry.isDirectory()) continue;
501
- await appendInstanceDir(path.join(baseDir, entry.name));
502
- }
503
- return instanceDirs;
504
- } catch (error) {
505
- if (error.code === "ENOENT") return [];
506
- throw error;
507
- }
508
- }
509
- async function detectWechatDevtoolsSecuritySettings(localDataDir) {
510
- for (const fileName of SETTINGS_STORAGE_FILE_NAMES) {
511
- const filePath = path.join(localDataDir, fileName);
512
- const security = normalizeWechatDevtoolsSecuritySettings((await readJsonObject(filePath)).security);
513
- if (security) return {
514
- ...security,
515
- updatedAt: (await fs.stat(filePath)).mtimeMs
516
- };
517
- }
518
- }
519
- async function trustWechatDevtoolsProject(localDataDir, projectPath) {
520
- const normalizedProjectPath = path.resolve(projectPath);
521
- const projectHash = createStorageHash(`project2_${normalizedProjectPath}`);
522
- const fileNames = [`localstorage_${projectHash}.json`, `ls_${projectHash}.json`];
523
- let trusted = false;
524
- for (const fileName of fileNames) {
525
- const projectFilePath = path.join(localDataDir, fileName);
526
- const current = await readJsonObject(projectFilePath);
527
- if (current.projectid !== normalizedProjectPath || current.projectpath !== normalizedProjectPath || typeof current.appid !== "string" || !current.appid || !isRecord(current.attr) || !isRecord(current.attr.setting)) continue;
528
- await writeJsonObject(projectFilePath, {
529
- ...current,
530
- isTrusted: true
531
- });
532
- trusted = true;
533
- }
534
- return trusted;
535
- }
536
- async function scanWechatDevtoolsServicePort(context) {
537
- const instanceDirs = await resolveWechatDevtoolsInstanceDirs(context.baseDir);
538
- let detectedSecurityCount = 0;
539
- let detectedServicePort = {};
540
- let detectedUpdatedAt = -Infinity;
541
- for (const instanceDir of instanceDirs) {
542
- const security = await detectWechatDevtoolsSecuritySettings(path.join(instanceDir, "WeappLocalData"));
543
- if (!security) continue;
544
- detectedSecurityCount += 1;
545
- const candidate = {
546
- enabled: security.enableServicePort,
547
- port: security.port
548
- };
549
- if (shouldPreferServicePortCandidate(detectedServicePort, candidate, detectedUpdatedAt, security.updatedAt)) {
550
- detectedServicePort = candidate;
551
- detectedUpdatedAt = security.updatedAt;
552
- }
553
- }
554
- return {
555
- instanceDirs,
556
- touchedInstanceCount: instanceDirs.length,
557
- detectedSecurityCount,
558
- servicePort: detectedServicePort.port,
559
- servicePortEnabled: detectedServicePort.enabled
560
- };
561
- }
562
- /**
563
- * @description 检测微信开发者工具当前服务端口配置,严格沿用用户已有设置。
564
- */
565
- async function detectWechatDevtoolsServicePort(options = {}) {
566
- const context = createResolvedWechatDevtoolsContext(options);
567
- if (!context) return {
568
- touchedInstanceCount: 0,
569
- detectedSecurityCount: 0,
570
- servicePort: void 0,
571
- servicePortEnabled: void 0
572
- };
573
- const result = await scanWechatDevtoolsServicePort(context);
574
- return {
575
- touchedInstanceCount: result.touchedInstanceCount,
576
- detectedSecurityCount: result.detectedSecurityCount,
577
- servicePort: result.servicePort,
578
- servicePortEnabled: result.servicePortEnabled
579
- };
580
- }
581
- /**
582
- * @description 在启动微信开发者工具前,检测服务端口配置,并按需写入项目信任信息。
583
- */
584
- async function bootstrapWechatDevtoolsSettings(options = {}) {
585
- const context = createResolvedWechatDevtoolsContext(options);
586
- if (!context) return {
587
- touchedInstanceCount: 0,
588
- detectedSecurityCount: 0,
589
- updatedSecurityCount: 0,
590
- trustedProjectCount: 0,
591
- servicePort: void 0,
592
- servicePortEnabled: void 0
593
- };
594
- const scanResult = await scanWechatDevtoolsServicePort(context);
595
- let trustedProjectCount = 0;
596
- for (const instanceDir of scanResult.instanceDirs) {
597
- const localDataDir = path.join(instanceDir, "WeappLocalData");
598
- if (options.projectPath && options.trustProject !== false) {
599
- if (await trustWechatDevtoolsProject(localDataDir, options.projectPath)) trustedProjectCount += 1;
600
- }
601
- }
602
- return {
603
- touchedInstanceCount: scanResult.touchedInstanceCount,
604
- detectedSecurityCount: scanResult.detectedSecurityCount,
605
- updatedSecurityCount: 0,
606
- trustedProjectCount,
607
- servicePort: scanResult.servicePort,
608
- servicePortEnabled: scanResult.servicePortEnabled
609
- };
610
- }
611
- //#endregion
612
- //#region src/cli/automator.ts
613
- const ERROR_STACK_PREFIX_RE = /^at /;
614
- const ERROR_PREFIX_RE = /^\[error\]\s*/i;
615
- const ERROR_LABEL_PREFIX_RE = /^error\s*:\s*/i;
616
- const ERROR_LINE_SPLIT_RE = /\r?\n/;
617
- const LOGIN_REQUIRED_CN_RE = /需要重新登录/;
618
- const LOGIN_REQUIRED_EN_RE = /need\s+re-?login|re-?login/i;
619
- const LOGIN_REQUIRED_CODE_RE = /code\s*[:=]\s*(\d+)/i;
620
- const DEVTOOLS_HTTP_PORT_ERROR = "Failed to launch wechat web devTools, please make sure http port is open";
621
- const DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE = /Extension context invalidated/i;
622
- const AUTOMATOR_LAUNCH_TIMEOUT_RE = /Wait timed out after \d+ ms/i;
623
- const AUTOMATOR_WS_CONNECT_RE = /Failed connecting to ws:\/\/127\.0\.0\.1:\d+/i;
624
- const AUTOMATOR_PORT_IN_USE_RE = /Port \d+ is in use, please specify another port/i;
625
- const DEVTOOLS_PROTOCOL_TIMEOUT_RE = /DevTools did not respond to protocol method (\S+) within \d+ms/i;
626
- const DEVTOOLS_INFRA_ERROR_PATTERNS = [
627
- /#initialize-error:\s*wait IDE port timeout/i,
628
- /wait IDE port timeout/i,
629
- /listen EPERM/i,
630
- /operation not permitted 0\.0\.0\.0/i,
631
- /EACCES/i,
632
- /ECONNREFUSED/i,
633
- /connect ECONNREFUSED/i
634
- ];
635
- const DEFAULT_WECHAT_DEVTOOLS_WS_PORT = 9420;
636
- const PROJECT_AUTOMATOR_PORT_BASE = 9620;
637
- const PROJECT_AUTOMATOR_PORT_RANGE = 2e3;
638
- const AUTOMATOR_SESSION_DIR = path.join(os.tmpdir(), "weapp-vite-automator-sessions");
639
- const DEVTOOLS_LOGIN_REQUIRED_PATTERNS = [
640
- /code\s*[:=]\s*10/i,
641
- /需要重新登录/,
642
- /need\s+re-?login/i,
643
- /re-?login/i
644
- ];
645
- /**
646
- * @description 从错误对象中提取可读文本。
647
- */
648
- function extractErrorText(error) {
649
- if (!error || typeof error !== "object") return "";
650
- const candidate = error;
651
- return [
652
- candidate.message,
653
- candidate.shortMessage,
654
- candidate.stderr,
655
- candidate.stdout
656
- ].filter((value) => typeof value === "string" && value.trim().length > 0).join("\n");
657
- }
658
- function normalizeAutomatorSessionId(sessionId, port) {
659
- if (sessionId?.trim()) return sessionId.trim();
660
- return port ? `port-${port}` : "default";
661
- }
662
- /**
663
- * @description 为项目路径派生稳定的 DevTools automator 端口,避免多个 dev:open 项目抢占默认端口。
664
- */
665
- function resolveProjectAutomatorPort(projectPath) {
666
- const offset = createHash("sha1").update(path.resolve(projectPath)).digest().readUInt32BE(0) % PROJECT_AUTOMATOR_PORT_RANGE;
667
- return PROJECT_AUTOMATOR_PORT_BASE + offset;
668
- }
669
- function resolveAutomatorSessionFilePath(projectPath, sessionId, port) {
670
- const normalizedProjectPath = path.resolve(projectPath);
671
- const normalizedSessionId = normalizeAutomatorSessionId(sessionId, port);
672
- const sessionKey = normalizedSessionId === "default" ? normalizedProjectPath : `${normalizedProjectPath}#${normalizedSessionId}`;
673
- const encodedProjectPath = Buffer.from(sessionKey).toString("base64url");
674
- return path.join(AUTOMATOR_SESSION_DIR, `${encodedProjectPath}.json`);
675
- }
676
- async function persistAutomatorSession(options) {
677
- const filePath = resolveAutomatorSessionFilePath(options.projectPath, options.sessionId, options.port);
678
- const payload = {
679
- ...options.port ? { port: options.port } : {},
680
- projectPath: path.resolve(options.projectPath),
681
- ...options.sessionId ? { sessionId: options.sessionId } : {},
682
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
683
- wsEndpoint: options.wsEndpoint
684
- };
685
- await fs.mkdir(path.dirname(filePath), { recursive: true });
686
- await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
687
- }
688
- async function readPersistedAutomatorSession(projectPath, sessionId, port) {
689
- const filePath = resolveAutomatorSessionFilePath(projectPath, sessionId, port);
690
- try {
691
- const raw = await fs.readFile(filePath, "utf8");
692
- const payload = JSON.parse(raw);
693
- if (payload.projectPath !== path.resolve(projectPath) || typeof payload.wsEndpoint !== "string" || !payload.wsEndpoint.trim()) return null;
694
- if (sessionId && payload.sessionId !== sessionId) return null;
695
- if (port && payload.port !== port) return null;
696
- return payload;
697
- } catch {
698
- return null;
699
- }
700
- }
701
- async function removePersistedAutomatorSession(projectPath, sessionId, port) {
702
- const filePath = resolveAutomatorSessionFilePath(projectPath, sessionId, port);
703
- try {
704
- await fs.rm(filePath, { force: true });
705
- } catch {}
706
- }
707
- /**
708
- * @description 提取登录失效时最适合展示给用户的一行信息。
709
- */
710
- function extractLoginRequiredMessage$1(text) {
711
- if (!text) return "";
712
- if (LOGIN_REQUIRED_CN_RE.test(text)) return "需要重新登录";
713
- const englishMatch = text.match(LOGIN_REQUIRED_EN_RE);
714
- if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
715
- const firstLine = text.split(ERROR_LINE_SPLIT_RE).map((line) => line.trim()).find((line) => Boolean(line) && !ERROR_STACK_PREFIX_RE.test(line));
716
- if (!firstLine) return "";
717
- return firstLine.replace(ERROR_PREFIX_RE, "").replace(ERROR_LABEL_PREFIX_RE, "").slice(0, 120);
718
- }
719
- /**
720
- * @description 判断错误是否属于开发者工具服务端口不可用。
721
- */
722
- function isDevtoolsHttpPortError(error) {
723
- const message = error instanceof Error ? error.message : String(error);
724
- return message.includes(DEVTOOLS_HTTP_PORT_ERROR) || DEVTOOLS_INFRA_ERROR_PATTERNS.some((pattern) => pattern.test(message));
725
- }
726
- /**
727
- * @description 判断错误是否属于开发者工具 automator 扩展上下文尚未就绪。
728
- */
729
- function isDevtoolsExtensionContextInvalidatedError(error) {
730
- const message = error instanceof Error ? error.message : String(error);
731
- return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message);
732
- }
733
- /**
734
- * @description 判断错误是否属于可重试的 automator 启动抖动。
735
- */
736
- function isRetryableAutomatorLaunchError(error) {
737
- const message = error instanceof Error ? error.message : String(error);
738
- return DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED_RE.test(message) || AUTOMATOR_LAUNCH_TIMEOUT_RE.test(message) || AUTOMATOR_WS_CONNECT_RE.test(message);
739
- }
740
- /**
741
- * @description 判断错误是否属于开发者工具 websocket 连接失败。
742
- */
743
- function isAutomatorWsConnectError(error) {
744
- const message = error instanceof Error ? error.message : String(error);
745
- return AUTOMATOR_WS_CONNECT_RE.test(message);
746
- }
747
- /**
748
- * @description 判断错误是否属于目标项目 automator 端口仍被旧会话占用。
749
- */
750
- function isAutomatorPortInUseError(error) {
751
- const message = error instanceof Error ? error.message : String(error);
752
- return AUTOMATOR_PORT_IN_USE_RE.test(message);
753
- }
754
- /**
755
- * @description 判断错误是否属于开发者工具协议调用超时。
756
- */
757
- function isAutomatorProtocolTimeoutError(error) {
758
- const message = error instanceof Error ? error.message : String(error);
759
- return DEVTOOLS_PROTOCOL_TIMEOUT_RE.test(message);
760
- }
761
- /**
762
- * @description 提取协议超时的方法名。
763
- */
764
- function getAutomatorProtocolTimeoutMethod(error) {
765
- return (error instanceof Error ? error.message : String(error)).match(DEVTOOLS_PROTOCOL_TIMEOUT_RE)?.[1];
766
- }
767
- /**
768
- * @description 判断错误是否属于开发者工具登录失效。
769
- */
770
- function isAutomatorLoginError(error) {
771
- const text = extractErrorText(error);
772
- if (!text) return false;
773
- return DEVTOOLS_LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
774
- }
775
- /**
776
- * @description 格式化开发者工具登录失效错误,便于终端展示。
777
- */
778
- function formatAutomatorLoginError(error) {
779
- const text = extractErrorText(error);
780
- const code = text.match(LOGIN_REQUIRED_CODE_RE)?.[1];
781
- const message = extractLoginRequiredMessage$1(text);
782
- const lines = ["微信开发者工具返回登录错误:"];
783
- if (code) lines.push(`- code: ${code}`);
784
- if (message) lines.push(`- message: ${message}`);
785
- if (!code && !message) lines.push("- message: 需要重新登录");
786
- return lines.join("\n");
787
- }
788
- /**
789
- * @description 基于当前配置解析 CLI 路径,并通过现代化 automator 入口启动会话。
790
- */
791
- async function launchAutomator(options) {
792
- const { cliPath, port, projectPath, sessionId, timeout = 3e4 } = options;
793
- const resolvedCliPath = cliPath ?? (await resolveCliPath()).cliPath ?? void 0;
794
- const config = await readCustomConfig();
795
- const resolvedTrustProject = options.trustProject ?? config.autoTrustProject ?? false;
796
- const launcher = new Launcher();
797
- let lastError = null;
798
- let bootstrapResult;
799
- const resolvedProject = options.preserveProjectRoot ? {
800
- projectPath: path.resolve(projectPath),
801
- sourceProjectPath: path.resolve(projectPath)
802
- } : await resolveAutomatorProjectPath(projectPath);
803
- if (config.autoBootstrapDevtools !== false) bootstrapResult = await bootstrapWechatDevtoolsSettings({
804
- projectPath: resolvedProject.projectPath,
805
- trustProject: resolvedTrustProject
806
- });
807
- if (bootstrapResult?.servicePortEnabled === false) throw new Error("Detected WeChat DevTools service port is disabled in current settings. Please enable it manually; existing user settings were not modified.");
808
- const launchStartedAt = Date.now();
809
- for (let attempt = 0; attempt < 2; attempt += 1) try {
810
- const miniProgram = await launcher.launch({
811
- cliPath: resolvedCliPath,
812
- ...port ? { port } : {},
813
- projectPath: resolvedProject.projectPath,
814
- timeout,
815
- trustProject: resolvedTrustProject
816
- });
817
- const sessionMetadata = Reflect.get(miniProgram, "__WEAPP_VITE_SESSION_METADATA");
818
- if (typeof sessionMetadata?.wsEndpoint === "string" && sessionMetadata.wsEndpoint) {
819
- await persistAutomatorSession({
820
- ...port ? { port: sessionMetadata.port ?? port } : {},
821
- projectPath,
822
- sessionId,
823
- wsEndpoint: sessionMetadata.wsEndpoint
824
- });
825
- if (options.persistAsDefaultSession && (port || sessionId)) await persistAutomatorSession({
826
- projectPath,
827
- wsEndpoint: sessionMetadata.wsEndpoint
828
- });
829
- }
830
- return miniProgram;
831
- } catch (error) {
832
- lastError = error;
833
- if (!isRetryableAutomatorLaunchError(error) || attempt === 1 || Date.now() - launchStartedAt >= timeout) throw error;
834
- }
835
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
836
- }
837
- /**
838
- * @description 连接当前项目已打开的开发者工具自动化会话,不触发新的 IDE 拉起。
839
- */
840
- async function connectOpenedAutomator(options) {
841
- const { port, projectPath, sessionId, timeout } = options;
842
- const launcher = new Launcher();
843
- const persistedSession = await readPersistedAutomatorSession(projectPath, sessionId, port);
844
- const wsEndpoint = persistedSession?.wsEndpoint ?? (port ? `ws://127.0.0.1:${port}` : `ws://127.0.0.1:${DEFAULT_WECHAT_DEVTOOLS_WS_PORT}`);
845
- try {
846
- return await launcher.connect({
847
- timeout,
848
- wsEndpoint
849
- });
850
- } catch (error) {
851
- if (persistedSession) await removePersistedAutomatorSession(projectPath, sessionId, port);
852
- throw error;
853
- }
854
- }
855
- //#endregion
856
- //#region src/i18n.ts
857
- let currentLocale = "zh";
858
- /**
859
- * @description 设置当前语言。
860
- */
861
- function setLocale(locale) {
862
- currentLocale = normalizeLocale(locale);
863
- }
864
- /**
865
- * @description 从 argv 和环境变量中解析语言并设置。
866
- */
867
- function configureLocaleFromArgv(argv, fallbackLocale) {
868
- const localeFromArgv = readLangOption(argv);
869
- if (localeFromArgv) {
870
- setLocale(localeFromArgv);
871
- return currentLocale;
872
- }
873
- if (fallbackLocale) {
874
- setLocale(fallbackLocale);
875
- return currentLocale;
876
- }
877
- const localeFromEnv = process.env.WEAPP_IDE_CLI_LANG || process.env.LANG;
878
- if (localeFromEnv) {
879
- setLocale(localeFromEnv);
880
- return currentLocale;
881
- }
882
- setLocale("zh");
883
- return currentLocale;
884
- }
885
- /**
886
- * @description 根据当前语言选择文案。
887
- */
888
- function i18nText(zh, en) {
889
- return currentLocale === "en" ? en : zh;
890
- }
891
- /**
892
- * @description 校验 --lang 参数是否合法。
893
- */
894
- function validateLocaleOption(argv) {
895
- const localeFromArgv = readLangOption(argv);
896
- if (!localeFromArgv) return;
897
- if (!isLocaleTokenSupported(localeFromArgv)) throw new Error(`不支持的语言: ${localeFromArgv},仅支持 zh 或 en`);
898
- }
899
- function normalizeLocale(raw) {
900
- if (!raw) return "zh";
901
- const normalized = raw.trim().toLowerCase();
902
- if (normalized === "en" || normalized.startsWith("en_") || normalized.startsWith("en-")) return "en";
903
- return "zh";
904
- }
905
- function isLocaleTokenSupported(raw) {
906
- const normalized = raw.trim().toLowerCase();
907
- return normalized === "zh" || normalized === "en" || normalized.startsWith("zh_") || normalized.startsWith("zh-") || normalized.startsWith("en_") || normalized.startsWith("en-");
908
- }
909
- function readLangOption(argv) {
910
- for (let index = 0; index < argv.length; index += 1) {
911
- const token = argv[index];
912
- if (!token) continue;
913
- if (token === "--lang") {
914
- const value = argv[index + 1];
915
- return typeof value === "string" ? value : void 0;
916
- }
917
- if (token.startsWith("--lang=")) return token.slice(7);
918
- }
919
- }
920
- //#endregion
921
- //#region src/cli/inputCoordinator.ts
922
- const sharedSessions = /* @__PURE__ */ new Set();
923
- const exclusiveKeypressStack = [];
924
- const EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS = 500;
925
- let initialized = false;
926
- let rawModeEnabled = false;
927
- let dataListenerAttached = false;
928
- let keypressListenerAttached = false;
929
- let lastExclusiveResolvedKeypress;
930
- function hasInteractiveStdin() {
931
- return Boolean(process.stdin?.isTTY);
932
- }
933
- function canSetRawMode() {
934
- return typeof process.stdin?.setRawMode === "function";
935
- }
936
- function updateTerminalState() {
937
- const shouldKeepInputActive = Array.from(sharedSessions).some((session) => !session.closed && !session.suspended) || exclusiveKeypressStack.length > 0;
938
- if (canSetRawMode() && rawModeEnabled !== shouldKeepInputActive) {
939
- process.stdin.setRawMode(shouldKeepInputActive);
940
- rawModeEnabled = shouldKeepInputActive;
941
- }
942
- if (shouldKeepInputActive) {
943
- process.stdin.resume();
944
- return;
945
- }
946
- process.stdin.pause();
947
- }
948
- function handleData(chunk) {
949
- if (exclusiveKeypressStack.length > 0) return;
950
- for (const session of sharedSessions) {
951
- if (session.closed || session.suspended || !session.options.onData) continue;
952
- session.options.onData(chunk);
953
- }
954
- }
955
- function handleKeypress(str, key) {
956
- const activeExclusive = exclusiveKeypressStack[exclusiveKeypressStack.length - 1];
957
- if (activeExclusive) {
958
- const now = Date.now();
959
- const signature = `${key?.ctrl ? "ctrl+" : ""}${key?.name ?? str}`;
960
- if (lastExclusiveResolvedKeypress && lastExclusiveResolvedKeypress.signature === signature && now - lastExclusiveResolvedKeypress.timestamp <= EXCLUSIVE_KEYPRESS_CARRYOVER_GUARD_MS) {
961
- lastExclusiveResolvedKeypress.timestamp = now;
962
- return;
963
- }
964
- if (now < activeExclusive.ignoreUntil) return;
965
- const nextValue = activeExclusive.onKeypress(str, key);
966
- if (nextValue !== void 0) {
967
- clearTimeout(activeExclusive.timeout);
968
- exclusiveKeypressStack.pop();
969
- lastExclusiveResolvedKeypress = {
970
- signature,
971
- timestamp: now
972
- };
973
- updateTerminalState();
974
- activeExclusive.resolve(nextValue);
975
- }
976
- return;
977
- }
978
- for (const session of sharedSessions) {
979
- if (session.closed || session.suspended || !session.options.onKeypress) continue;
980
- session.options.onKeypress(str, key);
981
- }
982
- }
983
- function ensureCoordinatorInitialized() {
984
- if (initialized || !hasInteractiveStdin()) return;
985
- emitKeypressEvents(process.stdin);
986
- if (!dataListenerAttached) {
987
- process.stdin.on("data", handleData);
988
- dataListenerAttached = true;
989
- }
990
- if (!keypressListenerAttached) {
991
- process.stdin.on("keypress", handleKeypress);
992
- keypressListenerAttached = true;
993
- }
994
- initialized = true;
995
- }
996
- function createSharedInputSession(options) {
997
- if (!hasInteractiveStdin()) return;
998
- ensureCoordinatorInitialized();
999
- const session = {
1000
- closed: false,
1001
- options,
1002
- suspended: false
1003
- };
1004
- sharedSessions.add(session);
1005
- updateTerminalState();
1006
- return {
1007
- close() {
1008
- if (session.closed) return;
1009
- session.closed = true;
1010
- sharedSessions.delete(session);
1011
- updateTerminalState();
1012
- },
1013
- resume() {
1014
- if (session.closed) return;
1015
- session.suspended = false;
1016
- updateTerminalState();
1017
- },
1018
- suspend() {
1019
- if (session.closed) return;
1020
- session.suspended = true;
1021
- updateTerminalState();
1022
- }
1023
- };
1024
- }
1025
- async function waitForExclusiveKeypress(options) {
1026
- if (!hasInteractiveStdin()) return "timeout";
1027
- ensureCoordinatorInitialized();
1028
- return await new Promise((resolve) => {
1029
- const normalizedTimeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 3e4;
1030
- const normalizedIgnoreInitialMs = Number.isFinite(options.ignoreInitialMs) && options.ignoreInitialMs && options.ignoreInitialMs > 0 ? options.ignoreInitialMs : 0;
1031
- const record = {
1032
- ignoreUntil: Date.now() + normalizedIgnoreInitialMs,
1033
- onKeypress: options.onKeypress,
1034
- resolve,
1035
- timeout: setTimeout(() => {
1036
- const index = exclusiveKeypressStack.lastIndexOf(record);
1037
- if (index >= 0) exclusiveKeypressStack.splice(index, 1);
1038
- updateTerminalState();
1039
- resolve("timeout");
1040
- }, normalizedTimeoutMs)
1041
- };
1042
- exclusiveKeypressStack.push(record);
1043
- updateTerminalState();
1044
- });
1045
- }
1046
- async function runWithSuspendedSharedInput(runner) {
1047
- const previousStates = /* @__PURE__ */ new Map();
1048
- for (const session of sharedSessions) {
1049
- if (session.closed) continue;
1050
- previousStates.set(session, session.suspended);
1051
- session.suspended = true;
1052
- }
1053
- if (canSetRawMode() && rawModeEnabled) {
1054
- process.stdin.setRawMode(false);
1055
- rawModeEnabled = false;
1056
- }
1057
- process.stdin.resume();
1058
- try {
1059
- return await runner();
1060
- } finally {
1061
- for (const [session, suspended] of previousStates) if (!session.closed) session.suspended = suspended;
1062
- updateTerminalState();
1063
- }
1064
- }
1065
- //#endregion
1066
- //#region src/cli/retry.ts
1067
- const RETRY_PROMPT_INITIAL_IGNORE_MS = 300;
1068
- const RETRY_CONFIRM_KEYS = ["y"];
1069
- const RETRY_CANCEL_KEYS = [
1070
- "q",
1071
- "Esc",
1072
- "Ctrl+C"
1073
- ];
1074
- const LOGIN_REQUIRED_PATTERNS = [
1075
- /code\s*[:=]\s*10/i,
1076
- /需要重新登录/,
1077
- /need\s+re-?login/i,
1078
- /re-?login/i
1079
- ];
1080
- /**
1081
- * @description 提取执行错误文本,便于统一匹配与提示。
1082
- */
1083
- function extractExecutionErrorText(error) {
1084
- if (!error || typeof error !== "object") return "";
1085
- const parts = [];
1086
- const candidate = error;
1087
- for (const field of [
1088
- candidate.message,
1089
- candidate.shortMessage,
1090
- candidate.stderr,
1091
- candidate.stdout
1092
- ]) if (typeof field === "string" && field.trim()) parts.push(field);
1093
- return parts.join("\n");
1094
- }
1095
- function extractLoginRequiredMessage(text) {
1096
- if (!text) return "";
1097
- if (/需要重新登录/.test(text)) return "需要重新登录";
1098
- const englishMatch = text.match(/need\s+re-?login|re-?login/i);
1099
- if (englishMatch?.[0]) return englishMatch[0].toLowerCase();
1100
- const firstLine = text.split(/\r?\n/).map((line) => line.trim()).find((line) => Boolean(line) && !line.startsWith("at "));
1101
- if (!firstLine) return "";
1102
- return firstLine.replace(/^\[error\]\s*/i, "").replace(/^error\s*:\s*/i, "").slice(0, 120);
1103
- }
1104
- /**
1105
- * @description 判断是否为微信开发者工具登录失效错误。
1106
- */
1107
- function isWechatIdeLoginRequiredError(error) {
1108
- const text = extractExecutionErrorText(error);
1109
- if (!text) return false;
1110
- return LOGIN_REQUIRED_PATTERNS.some((pattern) => pattern.test(text));
1111
- }
1112
- /**
1113
- * @description 将登录失效错误格式化为更易读的摘要。
1114
- */
1115
- function formatWechatIdeLoginRequiredError(error) {
1116
- const text = extractExecutionErrorText(error);
1117
- const code = text.match(/code\s*[:=]\s*(\d+)/i)?.[1];
1118
- const message = extractLoginRequiredMessage(text);
1119
- const lines = ["微信开发者工具返回登录错误:"];
1120
- if (code) lines.push(`- code: ${code}`);
1121
- if (message) lines.push(`- message: ${message}`);
1122
- if (!code && !message) lines.push("- message: 需要重新登录");
1123
- return lines.join("\n");
1124
- }
1125
- /**
1126
- * @description 创建登录失效专用错误,并携带退出码语义。
1127
- */
1128
- function createWechatIdeLoginRequiredExitError(error, reason) {
1129
- const summary = formatWechatIdeLoginRequiredError(error);
1130
- const message = reason ? `${reason}\n${summary}` : summary;
1131
- const loginError = new Error(message);
1132
- loginError.name = "WechatIdeLoginRequiredError";
1133
- loginError.code = 10;
1134
- loginError.exitCode = 10;
1135
- return loginError;
1136
- }
1137
- /**
1138
- * @description 判断错误是否为登录失效专用退出错误。
1139
- */
1140
- function isWechatIdeLoginRequiredExitError(error) {
1141
- if (!error || typeof error !== "object") return false;
1142
- const candidate = error;
1143
- return candidate.name === "WechatIdeLoginRequiredError" || candidate.code === 10 || candidate.exitCode === 10;
1144
- }
1145
- /**
1146
- * @description 交互等待用户按 y 重试,按 q 或 Ctrl+C 取消。
1147
- */
1148
- async function waitForRetryKeypress(options = {}) {
1149
- const { timeoutMs = 3e4 } = options;
1150
- if (!process.stdin.isTTY) return "cancel";
1151
- return await waitForExclusiveKeypress({
1152
- ignoreInitialMs: 300,
1153
- onKeypress: (_str, key) => {
1154
- if (!key) return;
1155
- if (key.ctrl && key.name === "c") return "cancel";
1156
- if (key.name === "y") return "retry";
1157
- if (key.name === "q" || key.name === "escape") return "cancel";
1158
- },
1159
- timeoutMs
1160
- });
1161
- }
1162
- function highlightHotkey(key) {
1163
- return colors.bold(colors.green(key));
1164
- }
1165
- /**
1166
- * @description 生成重试按键提示,并高亮关键热键。
1167
- */
1168
- function formatRetryHotkeyPrompt(timeoutMs = 3e4) {
1169
- const highlight = (key) => highlightHotkey(key);
1170
- const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1e3));
1171
- const confirmKeys = RETRY_CONFIRM_KEYS.map(highlight).join(" / ");
1172
- const cancelKeys = RETRY_CANCEL_KEYS.map(highlight).join(" / ");
1173
- return i18nText(`按 ${confirmKeys} 重试,按 ${cancelKeys} 退出(${timeoutSeconds}s 内无输入将自动失败)。`, `Press ${confirmKeys} to retry, ${cancelKeys} to cancel (auto fail in ${timeoutSeconds}s).`);
1174
- }
1175
- /**
1176
- * @description 输出重试热键提示,并独占等待当前提示对应的按键输入。
1177
- */
1178
- async function promptRetryKeypress(options) {
1179
- const { logger, timeoutMs = 3e4 } = options;
1180
- logger.info(formatRetryHotkeyPrompt(timeoutMs));
1181
- return await waitForRetryKeypress({ timeoutMs });
1182
- }
1183
- /**
1184
- * @description 统一处理微信开发者工具登录失效场景下的提示与重试交互。
1185
- */
1186
- async function promptWechatIdeLoginRetry(options) {
1187
- const { allowRetry = true, cancelLevel = "info", error, logger, promptOpenIdeLogin = false, retryTimeoutMs = 3e4 } = options;
1188
- logger.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
1189
- if (promptOpenIdeLogin) logger.warn(i18nText("请先打开微信开发者工具完成登录。", "Please open Wechat DevTools and complete login first."));
1190
- logger.warn(formatWechatIdeLoginRequiredError(error));
1191
- if (!allowRetry) return "cancel";
1192
- const action = await promptRetryKeypress({
1193
- logger,
1194
- timeoutMs: retryTimeoutMs
1195
- });
1196
- if (action === "timeout") {
1197
- logger.error(i18nText(`等待登录重试输入超时(${retryTimeoutMs}ms),已自动取消。`, `Retry prompt timed out (${retryTimeoutMs}ms), canceled automatically.`));
1198
- return "cancel";
1199
- }
1200
- if (action !== "retry") {
1201
- logger[cancelLevel](i18nText("已取消重试。完成登录后请重新执行当前命令。", "Retry canceled. Please run the command again after login."));
1202
- return "cancel";
1203
- }
1204
- return "retry";
1205
- }
1206
- //#endregion
1207
- //#region src/cli/run-login-executor.ts
1208
- /**
1209
- * @description 执行可重试命令循环,并将“是否可重试”与“如何提示重试”交给调用方定义。
1210
- */
1211
- async function runRetryableCommand(options) {
1212
- const { createCancelError, execute, isRetryableResult, onCancel, onRetry, promptRetry, shouldRetry } = options;
1213
- let retryCount = 0;
1214
- while (true) {
1215
- const result = await execute();
1216
- if (!isRetryableResult(result)) return result;
1217
- if (shouldRetry(await promptRetry(result, retryCount))) {
1218
- retryCount += 1;
1219
- onRetry?.();
1220
- continue;
1221
- }
1222
- onCancel?.(result);
1223
- throw createCancelError(result);
1224
- }
1225
- }
1226
- //#endregion
1227
- //#region src/cli/automator-session.ts
1228
- function unwrapAutomatorConnectionError(result) {
1229
- return result.kind === "retryable" ? result.error : result.value;
1230
- }
1231
- async function launchFreshAutomator(options) {
1232
- const shouldUseProjectPort = !options.port && !options.sessionId;
1233
- return await launchAutomator({
1234
- ...options,
1235
- ...shouldUseProjectPort ? {
1236
- persistAsDefaultSession: true,
1237
- port: resolveProjectAutomatorPort(options.projectPath)
1238
- } : {}
1239
- });
1240
- }
1241
- function normalizeMiniProgramConnectionError(error, background = false) {
1242
- const diagnostics = background ? {
1243
- error: (message) => logger_default.debug(message),
1244
- warn: (message) => logger_default.debug(message)
1245
- } : logger_default;
1246
- if (isAutomatorLoginError(error)) {
1247
- diagnostics.error(i18nText("检测到微信开发者工具登录状态失效,请先登录后重试。", "Wechat DevTools login has expired. Please login and retry."));
1248
- diagnostics.warn(formatAutomatorLoginError(error));
1249
- return /* @__PURE__ */ new Error("DEVTOOLS_LOGIN_REQUIRED");
1250
- }
1251
- if (isDevtoolsHttpPortError(error)) {
1252
- diagnostics.error(i18nText("无法连接到微信开发者工具,请确保已开启 HTTP 服务端口。", "Cannot connect to Wechat DevTools. Please ensure HTTP service port is enabled."));
1253
- diagnostics.warn(i18nText("请在微信开发者工具中:设置 -> 安全设置 -> 开启服务端口", "Please enable service port in Wechat DevTools: Settings -> Security -> Service Port"));
1254
- return /* @__PURE__ */ new Error("DEVTOOLS_HTTP_PORT_ERROR");
1255
- }
1256
- if (isDevtoolsExtensionContextInvalidatedError(error)) {
1257
- diagnostics.error(i18nText("微信开发者工具自动化上下文尚未就绪,通常是刚启动或正在重载。", "Wechat DevTools automation context is not ready yet, usually because the IDE has just started or is still reloading."));
1258
- diagnostics.warn(i18nText("请稍后重试;若持续失败,关闭多余的开发者工具窗口后重试。", "Please retry shortly. If it keeps failing, close extra DevTools windows and try again."));
1259
- return /* @__PURE__ */ new Error("DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED");
1260
- }
1261
- if (isAutomatorWsConnectError(error) || isAutomatorPortInUseError(error)) {
1262
- diagnostics.error(i18nText("无法连接到当前项目的微信开发者工具自动化 websocket。", "Cannot connect to the Wechat DevTools automation websocket for the current project."));
1263
- diagnostics.warn(i18nText("请确认当前打开的是目标项目;若之前跑过其他 e2e / screenshot 任务,关闭多余的微信开发者工具窗口,或结束残留的 `wechatwebdevtools cli auto --project ...` 进程后重试。", "Please confirm the current DevTools window is the target project. If you recently ran other e2e / screenshot tasks, close extra windows or stop stale `wechatwebdevtools cli auto --project ...` processes and retry."));
1264
- return /* @__PURE__ */ new Error("DEVTOOLS_WS_CONNECT_ERROR");
1265
- }
1266
- if (isAutomatorProtocolTimeoutError(error)) {
1267
- const method = getAutomatorProtocolTimeoutMethod(error) ?? "unknown";
1268
- diagnostics.error(i18nText(`微信开发者工具在协议调用 ${method} 上超时,未按预期返回结果。`, `Wechat DevTools timed out while executing protocol method ${method} and did not return a result.`));
1269
- diagnostics.warn(i18nText("这通常表示当前 DevTools 自动化会话已卡住、窗口不在目标项目、或当前 DevTools 版本对该协议调用无响应。请重开目标项目窗口后重试;若仍复现,优先记录当前 DevTools 版本与协议方法名继续排查。", "This usually means the current DevTools automation session is stuck, the window is not on the target project, or the current DevTools version is not responding to that protocol method. Reopen the target project window and retry. If it still reproduces, record the current DevTools version and protocol method name for follow-up debugging."));
1270
- return /* @__PURE__ */ new Error("DEVTOOLS_PROTOCOL_TIMEOUT");
1271
- }
1272
- return error instanceof Error ? error : new Error(String(error));
1273
- }
1274
- /**
1275
- * @description 建立 automator 会话,并统一处理常见连接错误提示。
1276
- */
1277
- async function connectMiniProgramWithDiagnostics(options, background) {
1278
- const result = await runRetryableCommand({
1279
- createCancelError: (result) => createWechatIdeLoginRequiredExitError(unwrapAutomatorConnectionError(result), "cancelled"),
1280
- execute: async () => {
1281
- if (options.preferOpenedSession === false && options.openedOnly !== true) try {
1282
- return {
1283
- kind: "result",
1284
- value: await launchFreshAutomator(options)
1285
- };
1286
- } catch (error) {
1287
- if (!isAutomatorLoginError(error)) throw normalizeMiniProgramConnectionError(error, background);
1288
- return {
1289
- error,
1290
- kind: "retryable"
1291
- };
1292
- }
1293
- try {
1294
- return {
1295
- kind: "result",
1296
- value: await connectOpenedAutomator(options)
1297
- };
1298
- } catch (error) {
1299
- const normalizedOpenSessionError = normalizeMiniProgramConnectionError(error, background);
1300
- if (options.openedOnly || options.preferOpenedSession !== false && Boolean(options.port || options.sessionId)) throw normalizedOpenSessionError;
1301
- if (normalizedOpenSessionError instanceof Error && normalizedOpenSessionError.message === "DEVTOOLS_PROTOCOL_TIMEOUT") throw normalizedOpenSessionError;
1302
- try {
1303
- return {
1304
- kind: "result",
1305
- value: await launchFreshAutomator(options)
1306
- };
1307
- } catch (launchError) {
1308
- if (!isAutomatorLoginError(launchError)) throw normalizeMiniProgramConnectionError(launchError, background);
1309
- return {
1310
- error: launchError,
1311
- kind: "retryable"
1312
- };
1313
- }
1314
- }
1315
- },
1316
- isRetryableResult: (result) => result.kind === "retryable",
1317
- onRetry: () => {
1318
- logger_default.info(i18nText("正在重试连接微信开发者工具...", "Retrying to connect Wechat DevTools..."));
1319
- },
1320
- promptRetry: async (result) => await promptWechatIdeLoginRetry({
1321
- error: unwrapAutomatorConnectionError(result),
1322
- logger: logger_default,
1323
- promptOpenIdeLogin: true
1324
- }),
1325
- shouldRetry: (action) => action === "retry"
1326
- });
1327
- if (result.kind === "retryable") throw createWechatIdeLoginRequiredExitError(result.error);
1328
- return result.value;
1329
- }
1330
- /** 建立交互式会话,并报告最终连接错误。 */
1331
- async function connectMiniProgram(options) {
1332
- return await connectMiniProgramWithDiagnostics(options, false);
1333
- }
1334
- /** 后台日志桥接的连接探针,不在每次可恢复失败时输出终局错误。 */
1335
- async function connectConsoleMiniProgram(options) {
1336
- return await connectMiniProgramWithDiagnostics(options, true);
1337
- }
1338
- /** 为后台日志桥接保留共享会话所有权和诊断级别。 */
1339
- async function acquireConsoleMiniProgram(options) {
1340
- return await acquireSharedMiniProgram({
1341
- connectMiniProgram: connectConsoleMiniProgram,
1342
- normalizeConnectionError: (error) => normalizeMiniProgramConnectionError(error, true)
1343
- }, options);
1344
- }
1345
- const runtimeHooks = {
1346
- connectMiniProgram,
1347
- normalizeConnectionError: normalizeMiniProgramConnectionError
1348
- };
1349
- /**
1350
- * @description 获取指定项目的共享 automator 会话;若不存在则自动创建。
1351
- */
1352
- async function acquireSharedMiniProgram$1(options) {
1353
- return await acquireSharedMiniProgram(runtimeHooks, options);
1354
- }
1355
- /**
1356
- * @description 统一管理 automator 会话生命周期。
1357
- */
1358
- async function withMiniProgram$1(options, runner) {
1359
- return await withMiniProgram(runtimeHooks, options, runner);
1360
- }
1361
- //#endregion
1362
- export { createAutoTrustProjectConfig as $, getAutomatorProtocolTimeoutMethod as A, bootstrapWechatDevtoolsSettings as B, runWithSuspendedSharedInput as C, validateLocaleOption as D, i18nText as E, isDevtoolsExtensionContextInvalidatedError as F, resolveDevtoolsAutomationDefaults as G, resolveCliPath as H, isDevtoolsHttpPortError as I, isOperatingSystemSupported as J, SupportedPlatformsMap as K, isRetryableAutomatorLaunchError as L, isAutomatorPortInUseError as M, isAutomatorProtocolTimeoutError as N, connectOpenedAutomator as O, isAutomatorWsConnectError as P, createAutoBootstrapDevtoolsConfig as Q, launchAutomator as R, createSharedInputSession as S, configureLocaleFromArgv as T, getConfig as U, detectWechatDevtoolsServicePort as V, getConfiguredLocale as W, colors as X, operatingSystemName as Y, logger_default as Z, isWechatIdeLoginRequiredError as _, connectMiniProgram as a, defaultCustomConfigDirPath as at, promptWechatIdeLoginRetry as b, withMiniProgram$1 as c, RETRY_CONFIRM_KEYS as d, createCustomConfig as et, RETRY_PROMPT_INITIAL_IGNORE_MS as f, formatWechatIdeLoginRequiredError as g, formatRetryHotkeyPrompt as h, connectConsoleMiniProgram as i, removeCustomConfigKey as it, isAutomatorLoginError as j, formatAutomatorLoginError as k, runRetryableCommand as l, extractExecutionErrorText as m, acquireSharedMiniProgram$1 as n, overwriteCustomConfig as nt, getSharedMiniProgramSessionCount as o, defaultCustomConfigFilePath as ot, createWechatIdeLoginRequiredExitError as p, getDefaultCliPath as q, closeSharedMiniProgram as r, readCustomConfig as rt, releaseSharedMiniProgram as s, resolvePath as st, acquireConsoleMiniProgram as t, createLocaleConfig as tt, RETRY_CANCEL_KEYS as u, isWechatIdeLoginRequiredExitError as v, waitForExclusiveKeypress as w, waitForRetryKeypress as x, promptRetryKeypress as y, resolveProjectAutomatorPort as z };