weapp-vite 7.4.0 → 7.5.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.
Files changed (92) hide show
  1. package/README.md +51 -1
  2. package/bin/bootstrap.js +14 -1
  3. package/dist/adapters-CcHrsYaj.mjs +1 -0
  4. package/dist/analyze-ZJUvEdaY.mjs +248 -0
  5. package/dist/auto-import-components/resolvers.mjs +1 -2
  6. package/dist/auto-import-presets/wevu-router.mjs +1 -2
  7. package/dist/auto-import-presets/wevu.mjs +1 -2
  8. package/dist/auto-routes.mjs +2 -1
  9. package/dist/budget-BD7VqLCl.d.mts +178 -0
  10. package/dist/chunkStrategy-DRA5WpB-.mjs +1436 -0
  11. package/dist/cli-acceptance.d.mts +1 -0
  12. package/dist/cli-acceptance.mjs +21 -0
  13. package/dist/cli.mjs +1420 -1554
  14. package/dist/compatibility.mjs +1 -2
  15. package/dist/{config-Dbvj5yK7.d.mts → config-V0L-IAJf.d.mts} +87 -30
  16. package/dist/config.d.mts +2 -2
  17. package/dist/config.mjs +1 -1
  18. package/dist/{constants-WOsG3Eqd.mjs → constants-DEZdcsMn.mjs} +1 -1
  19. package/dist/{createContext-UOEgeYnA.mjs → createCompilerContextInstance-DJH_P_nZ.mjs} +31278 -32148
  20. package/dist/createContext-BqYzHhY9.mjs +12 -0
  21. package/dist/dashboard/analyze.d.mts +48 -0
  22. package/dist/dashboard/analyze.mjs +1 -0
  23. package/dist/dashboard/vite.d.mts +1 -1
  24. package/dist/dashboard-DvqF3Key.mjs +1842 -0
  25. package/dist/dashboard.d.mts +3 -2
  26. package/dist/dashboard.mjs +1 -2
  27. package/dist/dashboardViteBridge-Dy3nAD4m.mjs +299 -0
  28. package/dist/dev-fR5GiJRO.mjs +63 -0
  29. package/dist/devProvider-BGBkw28J.mjs +1050 -0
  30. package/dist/docs/README.md +51 -1
  31. package/dist/docs/ai-workflows.md +4 -0
  32. package/dist/docs/doctor.md +38 -0
  33. package/dist/docs/getting-started.md +3 -1
  34. package/dist/docs/index.md +3 -1
  35. package/dist/docs/mcp.md +94 -1
  36. package/dist/docs/testing.md +29 -2
  37. package/dist/docs/troubleshooting.md +8 -0
  38. package/dist/docs/uni-app-component-libraries.md +8 -7
  39. package/dist/docs/upload.md +40 -3
  40. package/dist/docs/vite-plugin.md +171 -0
  41. package/dist/docs/vue-sfc.md +19 -0
  42. package/dist/docs/weapp-config.md +54 -0
  43. package/dist/docs/wevu-authoring.md +10 -0
  44. package/dist/doctor-worker.d.mts +1 -0
  45. package/dist/doctor-worker.mjs +59 -0
  46. package/dist/doctor.d.mts +144 -0
  47. package/dist/doctor.mjs +848 -0
  48. package/dist/eslint.mjs +1 -2
  49. package/dist/file-DRb4eWqw.mjs +1 -0
  50. package/dist/{file-giE9UMZa.mjs → file-sC3jD7Fh.mjs} +15 -9
  51. package/dist/files-BqxFIPRp.mjs +45 -0
  52. package/dist/getInstance-DiGSzBNI.mjs +28 -0
  53. package/dist/getInstance-SiE6WZRh.mjs +1 -0
  54. package/dist/index-C9HjUX5c.d.mts +741 -0
  55. package/dist/index.d.mts +10 -8
  56. package/dist/index.mjs +3 -2
  57. package/dist/json.d.mts +1 -1
  58. package/dist/json.mjs +1 -2
  59. package/dist/lifecycle-BROTlwSL.mjs +320 -0
  60. package/dist/loadConfig-C3BQhz7F.mjs +59 -0
  61. package/dist/loadConfig-VGtBF4qm.mjs +1 -0
  62. package/dist/logger-DGn5iEVN.mjs +1 -0
  63. package/dist/{mcp-BG6TliEg.mjs → mcp-Cj8Maqo6.mjs} +16 -7
  64. package/dist/mcp-D3Yh24Cj.mjs +529 -0
  65. package/dist/mcp.d.mts +6 -3
  66. package/dist/mcp.mjs +1 -2
  67. package/dist/package-7swhyLmf.mjs +91 -0
  68. package/dist/path-doV773b7.mjs +103 -0
  69. package/dist/{platform-DTWs5QRi.mjs → platform-CnkySBKz.mjs} +1 -1
  70. package/dist/processCleanup-B6KA30Zc.mjs +77 -0
  71. package/dist/protocol-vg8HGJ4n.mjs +138 -0
  72. package/dist/{resolvers-icNxkpcy.mjs → resolvers-Dod-euqX.mjs} +4 -1
  73. package/dist/resolvers.mjs +1 -2
  74. package/dist/runtime.mjs +1 -2
  75. package/dist/session-PskhD-nZ.mjs +390 -0
  76. package/dist/test.d.mts +1 -0
  77. package/dist/test.mjs +201 -44
  78. package/dist/types.d.mts +2 -2
  79. package/dist/{upload-D0RcEWrq.mjs → upload-D4msblBf.mjs} +174 -15
  80. package/dist/upload-worker.mjs +136 -3
  81. package/dist/utils-CJSCgEj6.mjs +1094 -0
  82. package/dist/vite.d.mts +9 -0
  83. package/dist/vite.mjs +270 -0
  84. package/dist/volar.d.mts +2 -2
  85. package/dist/web-apis.mjs +1 -2
  86. package/package.json +67 -41
  87. package/dist/adapters-BwnMMIZR.mjs +0 -2
  88. package/dist/dashboard-BzaVesmQ.mjs +0 -558
  89. package/dist/file-D19nbDoN.mjs +0 -2
  90. package/dist/getInstance-CYF1vl5l.mjs +0 -2
  91. package/dist/index-DbqVupg0.d.mts +0 -246
  92. package/dist/logger-mt4mSTqV.mjs +0 -2
package/dist/cli.mjs CHANGED
@@ -1,44 +1,47 @@
1
1
  import { a as resolveWeappViteHostMeta } from "./pluginHost--CaeyWpA.mjs";
2
- import { C as collectPreloadPages, D as loadViteConfigFile, E as suggestPreloadRules, O as createCjsConfigLoadError, S as getProjectConfigFileName, T as resolvePreloadPackageIdentifier, _ as SHARED_CHUNK_VIRTUAL_PREFIX, a as formatBytes, b as resolveWeappConfigFile, d as getBackendForCapability, f as resolveBackendExecution, g as createSharedBuildConfig, h as createGlassEaselAnalyzeResult, k as isPathInside, m as syncManagedTsconfigBootstrapFiles, p as syncProjectSupportFiles, t as createCompilerContext, v as resolveHmrProfileJsonPath, w as findPreloadRuleKey, x as checkRuntime } from "./createContext-UOEgeYnA.mjs";
3
- import { l as getProjectPlatformOptions, n as getDefaultIdeProjectRoot, w as shouldPassPlatformArgToIdeOpen } from "./platform-DTWs5QRi.mjs";
4
- import { i as findJsEntry, o as findTemplateEntry, s as findVueEntry } from "./file-giE9UMZa.mjs";
5
- import { t as VERSION } from "./constants-WOsG3Eqd.mjs";
6
- import { r as logger_default, t as colors } from "./logger-mt4mSTqV.mjs";
7
- import { a as createDashboardArtifactSnapshot, i as resolveDashboardRoot, n as ANALYZE_DASHBOARD_PACKAGE_NAME, t as createAnalyzeDashboardDevframe } from "./dashboard-BzaVesmQ.mjs";
8
- import { n as resolveUploadPlatforms, r as redactUploadSecrets, t as prepareUpload } from "./upload-D0RcEWrq.mjs";
9
- import { c as startWeappViteMcpServer, l as detectAiDevelopmentEnvironment, s as resolveWeappMcpConfig } from "./mcp-BG6TliEg.mjs";
2
+ import { a as getDevShutdownScope, i as createDevShutdownScope, o as getDevServerCloseRecord, s as replaceDevServerClose } from "./lifecycle-BROTlwSL.mjs";
3
+ import { E as createSharedBuildConfig, S as createGlassEaselAnalyzeResult, c as getBackendForCapability, f as syncProjectSupportFiles, l as resolveBackendExecution, n as formatBytes, p as syncManagedTsconfigBootstrapFiles, u as CompilerSession, x as createDevViteServer } from "./createCompilerContextInstance-DJH_P_nZ.mjs";
4
+ import { l as resolveHmrProfileJsonPath } from "./devProvider-BGBkw28J.mjs";
5
+ import { l as getProjectPlatformOptions, n as getDefaultIdeProjectRoot, w as shouldPassPlatformArgToIdeOpen } from "./platform-CnkySBKz.mjs";
6
+ import { i as findJsEntry, o as findTemplateEntry, s as findVueEntry } from "./file-sC3jD7Fh.mjs";
7
+ import { t as VERSION } from "./constants-DEZdcsMn.mjs";
8
+ import { C as resolvePreloadPackageIdentifier, S as findPreloadRuleKey, c as getProjectConfigFileName, w as suggestPreloadRules, x as collectPreloadPages } from "./utils-CJSCgEj6.mjs";
9
+ import { r as logger_default, t as colors } from "./logger-DGn5iEVN.mjs";
10
+ import { t as createCompilerContext } from "./createContext-BqYzHhY9.mjs";
11
+ import { a as filterDuplicateOptions, c as resolveConfigFile, i as convertBase, l as registerAcceptCommand, n as formatMcpQuickStart, o as isUiEnabled, r as coerceBooleanOption, s as parseDashboardUiHost, t as registerMcpCommand } from "./mcp-D3Yh24Cj.mjs";
12
+ import { a as isRuntimeCategory, i as createAnalyzeBudgetCheck, r as createAnalyzeComparison, t as createDuplicateModuleInsights } from "./analyze-ZJUvEdaY.mjs";
13
+ import { i as resolveAnalyzeHistoryMetadata, n as createAnalyzeMetadata, t as terminateStaleSassEmbeddedProcess } from "./processCleanup-B6KA30Zc.mjs";
14
+ import { a as resolveModuleSourceType, i as resolveAssetSource, n as classifyPackage, r as normalizeModuleId, t as version } from "./package-7swhyLmf.mjs";
15
+ import { a as createDashboardArtifactSnapshot, i as resolveDashboardRoot, n as ANALYZE_DASHBOARD_PACKAGE_NAME, t as createAnalyzeDashboardDevframe } from "./dashboard-DvqF3Key.mjs";
16
+ import { a as terminateUploadProcess, i as terminateUploadDescendants, n as resolveUploadPlatforms, r as redactUploadSecrets, t as prepareUpload } from "./upload-D4msblBf.mjs";
17
+ import { t as loadConfig } from "./loadConfig-C3BQhz7F.mjs";
18
+ import { c as startWeappViteMcpServer, l as detectAiDevelopmentEnvironment, s as resolveWeappMcpConfig } from "./mcp-Cj8Maqo6.mjs";
19
+ import process from "node:process";
10
20
  import path, { posix } from "pathe";
11
- import fs from "node:fs";
21
+ import fs, { readFileSync } from "node:fs";
12
22
  import { fileURLToPath } from "node:url";
13
23
  import path$1 from "node:path";
14
24
  import { defu } from "@weapp-core/shared";
15
25
  import { fs as fs$1 } from "@weapp-core/shared/fs";
16
- import process from "node:process";
17
26
  import { parse } from "vue/compiler-sfc";
18
- import fs$2, { access, mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
19
- import { build, createServer } from "vite";
20
- import { randomUUID } from "node:crypto";
21
- import { parseArgs, parseEnv, promisify } from "node:util";
22
- import os from "node:os";
23
- import { execFile, fork, spawn } from "node:child_process";
27
+ import fs$2, { access, mkdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
28
+ import { build } from "vite";
29
+ import { createHash, randomUUID } from "node:crypto";
30
+ import { parseArgs } from "node:util";
24
31
  import { Buffer } from "node:buffer";
32
+ import { fork, spawn } from "node:child_process";
25
33
  import { cac } from "cac";
26
- import { RETRY_CANCEL_KEYS, RETRY_CONFIRM_KEYS, bootstrapWechatDevtoolsSettings, buildWechatIdeNpm, clearWechatIdeCache, clearWechatIdeCacheByAutomator, closeSharedMiniProgram, closeWechatIdeProject, compileWechatIdeByAutomator, connectOpenedAutomator, createSharedInputSession, createWechatIdeLoginRequiredExitError, defaultCustomConfigFilePath, detectWechatDevtoolsServicePort, dispatchWechatCliCommand, formatAutomatorLoginError, getConfig, getWechatIdeTestAccounts, getWechatIdeTicket, getWechatIdeToolInfo, isAutomatorLoginError, isRetryableAutomatorLaunchError, isWeappIdeTopLevelCommand, isWechatIdeEngineBuildEndpointMissingError, isWechatIdeLoggedIn, isWechatIdeLoginRequiredError, isWechatIdeLoginRequiredExitError, launchAutomator, parse as parse$1, promptRetryKeypress, promptWechatIdeLoginRetry, quitWechatIde, refreshWechatIdeTicket, resetWechatIdeFileUtilsByHttp, resolveCliPath, resolveProjectAutomatorPort, runRetryableCommand, runWechatIdeEngineBuild, runWithSuspendedSharedInput, setWechatIdeTicket, startForwardConsole, takeScreenshot } from "weapp-ide-cli";
34
+ import { RETRY_CANCEL_KEYS, RETRY_CONFIRM_KEYS, bootstrapWechatDevtoolsSettings, buildWechatIdeNpm, clearWechatIdeCache, clearWechatIdeCacheByAutomator, closeSharedMiniProgram, closeWechatIdeProject, compileWechatIdeByAutomator, connectOpenedAutomator, createSharedInputSession, createWechatIdeLoginRequiredExitError, defaultCustomConfigFilePath, detectWechatDevtoolsServicePort, dispatchWechatCliCommand, formatAutomatorLoginError, getWechatIdeTestAccounts, getWechatIdeTicket, getWechatIdeToolInfo, isAutomatorLoginError, isRetryableAutomatorLaunchError, isWeappIdeTopLevelCommand, isWechatIdeEngineBuildEndpointMissingError, isWechatIdeLoginRequiredError, isWechatIdeLoginRequiredExitError, launchAutomator, parse as parse$1, promptRetryKeypress, promptWechatIdeLoginRetry, queryWechatIdeLogin, quitWechatIde, refreshWechatIdeTicket, resetWechatIdeFileUtilsByHttp, resolveCliPath, resolveProjectAutomatorPort, runRetryableCommand, runWechatIdeEngineBuild, runWithSuspendedSharedInput, setWechatIdeTicket, startForwardConsole, takeScreenshot } from "weapp-ide-cli";
27
35
  import { brotliCompressSync, gzipSync } from "node:zlib";
28
- import { defineRpcFunction } from "devframe";
29
36
  import { buildOtpAuthUrl, refreshTempAuthCode } from "devframe/node/auth";
30
37
  import { resolveCommand } from "package-manager-detector/commands";
31
- import { Server } from "node:http";
32
- import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_SSE_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
33
- import { initDevframe } from "devframe/initiate";
34
38
  import semverInc from "semver/functions/inc.js";
35
39
  import { x } from "tinyexec";
40
+ import { parse as parse$2 } from "dotenv";
36
41
  import { expand } from "dotenv-expand";
37
42
  import { generateJs, generateJson, generateWxml, generateWxss } from "@weapp-core/schematics";
38
43
  import { determineAgent } from "@vercel/detect-agent";
39
44
  import { initConfig } from "@weapp-core/init";
40
- import { createInterface } from "node:readline/promises";
41
- import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from "node:timers";
42
45
  import net from "node:net";
43
46
  //#region src/cli/runtime.ts
44
47
  function logRuntimeTarget(targets, options = {}) {
@@ -64,6 +67,9 @@ function resolveRuntimeTargets(options) {
64
67
  rawPlatform
65
68
  };
66
69
  }
70
+ function resolveConfiguredRuntimeTargets(targets, platform) {
71
+ return targets.rawPlatform === void 0 && platform === "web" ? resolveRuntimeTargets({ platform: "web" }) : targets;
72
+ }
67
73
  function createInlineConfig(execution, options = {}) {
68
74
  const configs = execution.entries.map((entry) => entry.driver.createInlineConfig({
69
75
  execution,
@@ -207,33 +213,10 @@ async function executeWechatIdeCliCommand(argv, options = {}) {
207
213
  }
208
214
  //#endregion
209
215
  //#region src/cli/openIde/close.ts
210
- const execFileAsync = promisify(execFile);
211
- async function closeIdeByAppleScript() {
212
- if (process.platform !== "darwin") return false;
213
- const appName = process.env.WEAPP_DEVTOOLS_APP_NAME || "wechatwebdevtools";
214
- try {
215
- await execFileAsync("osascript", ["-e", `tell application "${appName}" to quit`]);
216
- return true;
217
- } catch {
218
- return false;
219
- }
220
- }
221
- async function closeIdeByProcessKill(cliPath) {
222
- if (!cliPath) return false;
223
- const appContentsRoot = cliPath.includes(".app/") ? cliPath.slice(0, cliPath.indexOf(".app/") + 4) : path.dirname(path.dirname(cliPath));
224
- try {
225
- await execFileAsync("pkill", ["-f", appContentsRoot]);
226
- return true;
227
- } catch {
228
- return false;
229
- }
230
- }
231
216
  /**
232
- * @description 关闭微信开发者工具,并在 CLI 不可用时回退到系统级关闭。
217
+ * @description 执行用户显式请求的 CLI close;失败时保留宿主,不升级为系统级退出。
233
218
  */
234
219
  async function closeIde$1() {
235
- const config = await getConfig();
236
- const cliPath = config.cliPath?.trim() ? config.cliPath : null;
237
220
  try {
238
221
  await closeWechatIdeProject();
239
222
  return true;
@@ -249,17 +232,9 @@ async function closeIde$1() {
249
232
  logger_default.error(retryError);
250
233
  }
251
234
  else {
252
- logger_default.warn("微信开发者工具 CLI close 执行失败,尝试回退为系统级关闭。");
235
+ logger_default.warn("微信开发者工具 CLI close 执行失败,已保留现有宿主;请在开发者工具中手动关闭目标项目。");
253
236
  logger_default.error(error);
254
237
  }
255
- if (await closeIdeByAppleScript()) {
256
- logger_default.info("已回退为系统级关闭微信开发者工具。");
257
- return true;
258
- }
259
- if (await closeIdeByProcessKill(cliPath)) {
260
- logger_default.info("已回退为进程级关闭微信开发者工具。");
261
- return true;
262
- }
263
238
  return false;
264
239
  }
265
240
  }
@@ -308,7 +283,7 @@ function logWechatIdeServicePortDisabledHint(projectPath) {
308
283
  const OPENED_PROJECT_HEALTH_CHECK_TIMEOUT = 3e3;
309
284
  const OPEN_AUTOMATOR_TIMEOUT = 12e4;
310
285
  function formatReuseOpenedWechatIdePrompt() {
311
- return `目标项目已在微信开发者工具中打开,已跳过重复打开。按 ${RETRY_CONFIRM_KEYS.map((key) => colors.bold(colors.green(key))).join(" / ")} 关闭当前窗口后重新打开。`;
286
+ return `目标项目已在微信开发者工具中打开,已跳过重复打开。按 ${RETRY_CONFIRM_KEYS.map((key) => colors.bold(colors.green(key))).join(" / ")} 重新连接目标项目(保留现有窗口)。`;
312
287
  }
313
288
  function disconnectMiniProgram(miniProgram) {
314
289
  miniProgram.disconnect();
@@ -363,7 +338,7 @@ async function connectOpenedProject(projectPath) {
363
338
  /**
364
339
  * @description 若当前项目已在微信开发者工具中打开且自动化可连通,则直接复用现有会话,避免重复拉起 IDE。
365
340
  */
366
- async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
341
+ async function tryReuseOpenedWechatIde(projectPath, options = {}) {
367
342
  const connection = await connectOpenedProject(projectPath);
368
343
  if (connection.status === "unhealthy") return {
369
344
  reopened: false,
@@ -385,8 +360,7 @@ async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
385
360
  reopened: false,
386
361
  reused: true
387
362
  };
388
- logger_default.info(colors.bold(colors.green("正在关闭当前已打开项目,并重新拉起微信开发者工具...")));
389
- if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
363
+ logger_default.info(colors.bold(colors.green("正在重新连接目标项目,保留现有微信开发者工具窗口...")));
390
364
  await openWechatIdeByAutomator(projectPath, {
391
365
  preserveProjectRoot: options.preserveProjectRoot,
392
366
  trustProject: options.trustProject
@@ -397,16 +371,15 @@ async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
397
371
  };
398
372
  }
399
373
  /**
400
- * @description 对已打开的目标项目执行强制重开,以刷新最新构建产物。
374
+ * @description 重新连接目标项目以刷新构建产物;连接失败不授予关闭共享宿主的权限。
401
375
  */
402
- async function reopenOpenedWechatIde(projectPath, closeIde, options = {}) {
376
+ async function reopenOpenedWechatIde(projectPath, options = {}) {
403
377
  const connection = await connectOpenedProject(projectPath);
404
378
  if (connection.status === "connected") {
405
379
  disconnectMiniProgram(connection.miniProgram);
406
- logger_default.info("目标项目已在微信开发者工具中打开,当前命令将主动重开以刷新最新构建产物。");
407
- } else if (connection.status === "unhealthy") logger_default.info("目标项目的 automator 会话未通过健康检查,当前命令将关闭窗口并重新拉起。");
408
- else logger_default.info("未检测到可复用的 automator 会话,当前命令将关闭现有窗口并重新拉起目标项目。");
409
- if (!await (connection.status === "connected" ? closeIde : options.restartIde ?? closeIde)()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
380
+ logger_default.info("目标项目已在微信开发者工具中打开,当前命令将重新连接以刷新最新构建产物,保留现有窗口。");
381
+ } else if (connection.status === "unhealthy") logger_default.info("目标项目的 automator 会话未通过健康检查,将重试连接目标项目,保留现有窗口。");
382
+ else logger_default.info("未检测到可复用的 automator 会话,将尝试打开目标项目,保留其他窗口。");
410
383
  await openWechatIdeByAutomator(projectPath, options);
411
384
  return true;
412
385
  }
@@ -417,11 +390,7 @@ function shouldLogAutomatorFallbackError() {
417
390
  return flag === "1" || flag === "true";
418
391
  }
419
392
  const PREPARE_AUTOMATOR_SESSION_TIMEOUT = 8e3;
420
- const AUTOMATOR_RECOVERY_QUIT_SETTLE_MS = 5e3;
421
393
  const RESET_FILEUTILS_ENV = "WEAPP_VITE_RESET_IDE_FILEUTILS";
422
- function sleep$1(ms) {
423
- return new Promise((resolve) => setTimeout(resolve, ms));
424
- }
425
394
  function readProjectConfigObject(projectPath) {
426
395
  try {
427
396
  const raw = fs.readFileSync(path.join(projectPath, "project.config.json"), "utf8");
@@ -450,7 +419,10 @@ function shouldResetWechatIdeFileUtils() {
450
419
  async function runWechatIdeOpenWithRetry(argv) {
451
420
  await executeWechatIdeCliCommand(argv, {
452
421
  cancelLevel: "warn",
453
- onNonLoginError: (error) => logger_default.error(error),
422
+ onNonLoginError: (error) => {
423
+ logger_default.error(error);
424
+ if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL?.trim()) throw error;
425
+ },
454
426
  onRetry: () => {
455
427
  logger_default.info(colors.bold(colors.green("正在重试连接微信开发者工具...")));
456
428
  },
@@ -477,25 +449,15 @@ function resolveIdeProjectRoot(mpDistRoot, cwd, generatedProjectConfig = false)
477
449
  async function closeIde() {
478
450
  return await closeIde$1();
479
451
  }
480
- async function restartWechatIdeForAutomatorRecovery() {
481
- try {
482
- await quitWechatIde();
483
- await sleep$1(AUTOMATOR_RECOVERY_QUIT_SETTLE_MS);
484
- return true;
485
- } catch {
486
- return await closeIde();
487
- }
488
- }
489
452
  async function tryOpenWechatIdeByAutomator(projectPath, options) {
490
453
  const preserveProjectRoot = !shouldUseAutomatorProjectWrapper(projectPath);
491
454
  if (options.reuseOpenedProject === false) {
492
- if (await reopenOpenedWechatIde(projectPath, closeIde, {
455
+ if (await reopenOpenedWechatIde(projectPath, {
493
456
  preserveProjectRoot,
494
- restartIde: restartWechatIdeForAutomatorRecovery,
495
457
  trustProject: options.trustProject
496
458
  })) return "reopened";
497
459
  }
498
- const reuseResult = await tryReuseOpenedWechatIde(projectPath, closeIde, {
460
+ const reuseResult = await tryReuseOpenedWechatIde(projectPath, {
499
461
  preserveProjectRoot,
500
462
  promptReopen: options.reuseOpenedProject !== true,
501
463
  trustProject: options.trustProject
@@ -660,12 +622,11 @@ async function recoverOpenedWechatIdeProject(platform, projectPath, servicePortE
660
622
  logger_default.warn("已跳过微信开发者工具自动恢复;请按上方提示手动关闭并重新打开目标项目。");
661
623
  return failedResult;
662
624
  }
663
- logger_default.info(`检测到微信开发者工具打开后状态不稳定(${formatWechatIdeOpenHealthReason(failedResult)}),正在自动关闭并重新打开目标项目...`);
664
- if (!await closeIde()) logger_default.warn("自动恢复时关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
625
+ logger_default.info(`检测到微信开发者工具打开后状态不稳定(${formatWechatIdeOpenHealthReason(failedResult)}),正在重试打开目标项目(保留现有窗口)...`);
665
626
  await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, options));
666
627
  const recoveredResult = await verifyOpenedWechatIdeProject(projectPath, servicePortEnabled, options);
667
628
  if (recoveredResult.ok) logger_default.info("微信开发者工具已完成自动恢复。");
668
- else logger_default.warn("微信开发者工具自动恢复未完成;可设置 `WEAPP_VITE_DISABLE_IDE_OPEN_RECOVERY=1` 或传入 `--no-open-recovery` 跳过自动关闭重开,并按提示手动处理。");
629
+ else logger_default.warn("微信开发者工具自动恢复未完成;可设置 `WEAPP_VITE_DISABLE_IDE_OPEN_RECOVERY=1` 或传入 `--no-open-recovery` 跳过自动重试,并按提示手动处理。");
669
630
  return recoveredResult;
670
631
  }
671
632
  async function verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, servicePortEnabled, options) {
@@ -702,6 +663,7 @@ async function openIde(platform, projectPath, options = {}) {
702
663
  return;
703
664
  }
704
665
  } catch (error) {
666
+ if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL?.trim()) throw error;
705
667
  if (isAutomatorLoginError(error)) {
706
668
  logger_default.error("检测到微信开发者工具登录状态失效,请先登录后重试。");
707
669
  logger_default.warn(formatAutomatorLoginError(error));
@@ -709,9 +671,6 @@ async function openIde(platform, projectPath, options = {}) {
709
671
  logger_default.warn(`通过 automator 启动微信开发者工具失败,回退到普通 open 流程:${formatWechatIdeAutomatorFailure(error)}`);
710
672
  if (shouldLogAutomatorFallbackError()) logger_default.error(error);
711
673
  }
712
- else if (platform === "weapp" && projectPath && normalizedOptions.reuseOpenedProject === false) {
713
- if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试打开目标项目。");
714
- }
715
674
  await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, normalizedOptions));
716
675
  if (platform === "weapp" && projectPath && normalizedOptions.prepareAutomatorSession) await prepareWechatIdeAutomatorSession(projectPath, normalizedOptions);
717
676
  if (platform === "weapp" && projectPath && !normalizedOptions.skipPostOpenHealthCheck) await verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, bootstrapResult?.servicePortEnabled, normalizedOptions);
@@ -753,35 +712,6 @@ async function resolveIdeCommandContext(options) {
753
712
  };
754
713
  }
755
714
  //#endregion
756
- //#region src/cli/options.ts
757
- function filterDuplicateOptions(options) {
758
- for (const [key, value] of Object.entries(options)) if (Array.isArray(value)) options[key] = value[value.length - 1];
759
- }
760
- function resolveConfigFile(options) {
761
- if (typeof options.config === "string") return options.config;
762
- if (typeof options.c === "string") return options.c;
763
- }
764
- function convertBase(value) {
765
- if (value === 0) return "";
766
- return value;
767
- }
768
- function coerceBooleanOption(value) {
769
- if (value === void 0) return;
770
- if (typeof value === "boolean") return value;
771
- if (typeof value === "string") {
772
- const normalized = value.trim().toLowerCase();
773
- if (normalized === "") return true;
774
- if (normalized === "false" || normalized === "0" || normalized === "off" || normalized === "no") return false;
775
- if (normalized === "true" || normalized === "1" || normalized === "on" || normalized === "yes") return true;
776
- return true;
777
- }
778
- if (typeof value === "number") return value !== 0;
779
- return Boolean(value);
780
- }
781
- function isUiEnabled(options) {
782
- return Boolean(options.ui || options.analyze);
783
- }
784
- //#endregion
785
715
  //#region src/cli/commands/alipayExecute.ts
786
716
  function createSpawnOptions() {
787
717
  return {
@@ -867,6 +797,154 @@ function registerAlipayCommand(cli) {
867
797
  });
868
798
  }
869
799
  //#endregion
800
+ //#region src/analyze/hmr/attribution.ts
801
+ const nonnegative = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
802
+ function difference(whole, ...parts) {
803
+ return nonnegative(whole) && parts.every(nonnegative) && whole >= parts.reduce((sum, value) => sum + value, 0) ? whole - parts.reduce((sum, value) => sum + value, 0) : null;
804
+ }
805
+ /** 外层时段互不重叠;插件 hook 属于 bundler 内部的嵌套观测,不能再次累加。 */
806
+ function attributeHmrProfile(sample, external) {
807
+ const value = (key) => nonnegative(sample[key]) ? sample[key] : null;
808
+ const phases = sample.pipeline === "stateful" ? Object.fromEntries((sample.profileMode === "delivery" ? [
809
+ "sourceToBatchMs",
810
+ "deliveryQueueMs",
811
+ "prepareMs",
812
+ "commitQueueMs",
813
+ "commitMs",
814
+ "publishMs"
815
+ ] : [
816
+ "sourceToBatchMs",
817
+ "snapshotBuildMs",
818
+ "snapshotPublishMs"
819
+ ]).map((key) => [key, value(key)])) : {
820
+ batchWaitMs: value("batchWaitMs"),
821
+ queueWaitMs: value("queueWaitMs"),
822
+ otherBeforeBuildMs: difference(sample.watchToDirtyMs, sample.batchWaitMs, sample.queueWaitMs),
823
+ bundlerMs: value("bundlerMs")
824
+ };
825
+ const profileResidualMs = sample.pipeline === "stateful" ? difference(sample.totalMs, ...Object.values(phases)) : difference(sample.totalMs, sample.watchToDirtyMs, sample.bundlerMs);
826
+ let externalStatus = external ? "unknown" : "not-observed";
827
+ let sourceToWatcherMs = null;
828
+ let afterProfileToVisibleMs = null;
829
+ let eventId = null;
830
+ if (external && sample.schemaVersion === 1 && sample.correlation === "known" && sample.clock?.durations === "performance.now" && nonnegative(sample.clock.timeOrigin) && nonnegative(external.writtenAtEpochMs) && external.visibleAtEpochMs >= external.writtenAtEpochMs) {
831
+ const events = (sample.sourceEvents ?? []).filter((event) => event.file?.replaceAll("\\", "/") === external.sourceFile.replaceAll("\\", "/") && sample.clock.timeOrigin + event.receivedAtMs >= external.writtenAtEpochMs && sample.clock.timeOrigin + event.receivedAtMs <= external.visibleAtEpochMs);
832
+ if (events.length === 1 && sample.sourceEvents?.length === 1) {
833
+ const received = sample.clock.timeOrigin + events[0].receivedAtMs;
834
+ sourceToWatcherMs = received - external.writtenAtEpochMs;
835
+ afterProfileToVisibleMs = difference(external.visibleAtEpochMs - received, sample.totalMs);
836
+ eventId = events[0].eventId;
837
+ externalStatus = afterProfileToVisibleMs === null ? "unknown" : "matched";
838
+ }
839
+ }
840
+ return {
841
+ schemaVersion: 1,
842
+ pipeline: sample.pipeline ?? "standard",
843
+ status: Object.values(phases).every(nonnegative) && profileResidualMs !== null ? "complete" : "partial",
844
+ phases,
845
+ profileResidualMs,
846
+ external: {
847
+ status: externalStatus,
848
+ eventId,
849
+ sourceToWatcherMs,
850
+ afterProfileToVisibleMs
851
+ },
852
+ nestedMetrics: "hook durations overlap; do not add to outer phases"
853
+ };
854
+ }
855
+ //#endregion
856
+ //#region src/analyze/hmr/reader.ts
857
+ const stringFields = [
858
+ "timestamp",
859
+ "eventId",
860
+ "event",
861
+ "file",
862
+ "relativeFile",
863
+ "sourceRootFile",
864
+ "sessionId",
865
+ "buildId",
866
+ "batchId"
867
+ ];
868
+ const collectionFields = ["dirtyReasonSummary", "pendingReasonSummary"];
869
+ function isSourceEvent(value) {
870
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
871
+ const event = value;
872
+ return typeof event.eventId === "string" && (event.file === void 0 || typeof event.file === "string") && (event.event === void 0 || typeof event.event === "string" && [
873
+ "update",
874
+ "create",
875
+ "delete"
876
+ ].includes(event.event)) && typeof event.receivedAtMs === "number" && Number.isFinite(event.receivedAtMs) && event.receivedAtMs >= 0;
877
+ }
878
+ function isSample(value) {
879
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
880
+ const sample = value;
881
+ for (const [key, values] of Object.entries({
882
+ pipeline: ["standard", "stateful"],
883
+ profileMode: [
884
+ "delivery",
885
+ "full",
886
+ "refresh"
887
+ ],
888
+ completionBoundary: ["delivery-acknowledged", "output-published"]
889
+ })) if (sample[key] !== void 0 && (typeof sample[key] !== "string" || !values.includes(sample[key]))) return false;
890
+ if (typeof sample.totalMs !== "number" || !Number.isFinite(sample.totalMs) || sample.totalMs < 0) return false;
891
+ if (stringFields.some((key) => sample[key] !== void 0 && typeof sample[key] !== "string")) return false;
892
+ if (collectionFields.some((key) => {
893
+ const items = sample[key];
894
+ return items !== void 0 && (!Array.isArray(items) || items.some((item) => typeof item !== "string"));
895
+ })) return false;
896
+ if (sample.sourceEvents !== void 0 && (!Array.isArray(sample.sourceEvents) || !sample.sourceEvents.every(isSourceEvent))) return false;
897
+ return Object.entries(sample).every(([key, value]) => {
898
+ if (!key.endsWith("Ms") && !key.endsWith("Count")) return true;
899
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && (!key.endsWith("Count") || Number.isInteger(value));
900
+ });
901
+ }
902
+ /** 旧版仍可读取;不兼容、失败和未完成记录不参与正常耗时统计,缺失阶段不补零。 */
903
+ function readHmrProfileLines(content) {
904
+ const samples = [];
905
+ const coverage = {
906
+ legacy: 0,
907
+ compatible: 0,
908
+ incompatible: 0,
909
+ incomplete: 0,
910
+ invalid: 0
911
+ };
912
+ for (const line of content.split(/\r?\n/)) {
913
+ if (!line.trim()) continue;
914
+ let value;
915
+ try {
916
+ value = JSON.parse(line);
917
+ } catch {
918
+ coverage.invalid += 1;
919
+ continue;
920
+ }
921
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
922
+ coverage.invalid += 1;
923
+ continue;
924
+ }
925
+ const record = value;
926
+ if (record.schemaVersion !== void 0 && record.schemaVersion !== 1) {
927
+ coverage.incompatible += 1;
928
+ continue;
929
+ }
930
+ if (record.status === "incomplete" || record.status === "failed") {
931
+ coverage.incomplete += 1;
932
+ continue;
933
+ }
934
+ if (record.status !== void 0 && record.status !== "complete" || record.schemaVersion === 1 && record.status !== "complete" || !isSample(record)) {
935
+ coverage.invalid += 1;
936
+ continue;
937
+ }
938
+ coverage[record.schemaVersion === void 0 ? "legacy" : "compatible"] += 1;
939
+ samples.push(record);
940
+ }
941
+ return {
942
+ samples,
943
+ coverage,
944
+ skippedLineCount: coverage.incompatible + coverage.incomplete + coverage.invalid
945
+ };
946
+ }
947
+ //#endregion
870
948
  //#region src/analyze/hmr.ts
871
949
  function createMetricSummary(values) {
872
950
  if (!values.length) return { count: 0 };
@@ -905,26 +983,19 @@ function isFiniteNumber$1(value) {
905
983
  * @description 聚合 HMR JSONL profile,为命令行与后续仪表盘复用。
906
984
  */
907
985
  async function analyzeHmrProfile(options) {
908
- const lines = (await fs$1.readFile(options.profilePath, "utf8")).split(/\r?\n/);
909
- const samples = [];
910
- let skippedLineCount = 0;
911
- for (const line of lines) {
912
- const trimmed = line.trim();
913
- if (!trimmed) continue;
914
- try {
915
- const parsed = JSON.parse(trimmed);
916
- if (!isFiniteNumber$1(parsed.totalMs)) {
917
- skippedLineCount += 1;
918
- continue;
919
- }
920
- samples.push(parsed);
921
- } catch {
922
- skippedLineCount += 1;
923
- }
924
- }
986
+ const { samples, coverage: inputCoverage, skippedLineCount } = readHmrProfileLines(await fs$1.readFile(options.profilePath, "utf8"));
925
987
  const eventCounts = /* @__PURE__ */ new Map();
926
988
  const dirtyReasonCounts = /* @__PURE__ */ new Map();
927
989
  const pendingReasonCounts = /* @__PURE__ */ new Map();
990
+ const batchWaitValues = [];
991
+ const queueWaitValues = [];
992
+ const bundlerValues = [];
993
+ const finalizePrepareValues = [];
994
+ const finalizeTemplateValues = [];
995
+ const finalizePublishValues = [];
996
+ const publicationValidateValues = [];
997
+ const publicationIndependentValues = [];
998
+ const publicationPruneValues = [];
928
999
  const totalValues = [];
929
1000
  const buildCoreValues = [];
930
1001
  const buildStartValues = [];
@@ -974,6 +1045,15 @@ async function analyzeHmrProfile(options) {
974
1045
  const resolveCountValues = [];
975
1046
  const skippedLoadedCountValues = [];
976
1047
  for (const sample of samples) {
1048
+ if (isFiniteNumber$1(sample.batchWaitMs)) batchWaitValues.push(sample.batchWaitMs);
1049
+ if (isFiniteNumber$1(sample.queueWaitMs)) queueWaitValues.push(sample.queueWaitMs);
1050
+ if (isFiniteNumber$1(sample.bundlerMs)) bundlerValues.push(sample.bundlerMs);
1051
+ if (isFiniteNumber$1(sample.finalizePrepareMs)) finalizePrepareValues.push(sample.finalizePrepareMs);
1052
+ if (isFiniteNumber$1(sample.finalizeTemplateMs)) finalizeTemplateValues.push(sample.finalizeTemplateMs);
1053
+ if (isFiniteNumber$1(sample.finalizePublishMs)) finalizePublishValues.push(sample.finalizePublishMs);
1054
+ if (isFiniteNumber$1(sample.publicationValidateMs)) publicationValidateValues.push(sample.publicationValidateMs);
1055
+ if (isFiniteNumber$1(sample.publicationIndependentMs)) publicationIndependentValues.push(sample.publicationIndependentMs);
1056
+ if (isFiniteNumber$1(sample.publicationPruneMs)) publicationPruneValues.push(sample.publicationPruneMs);
977
1057
  totalValues.push(sample.totalMs);
978
1058
  if (sample.event) eventCounts.set(sample.event, (eventCounts.get(sample.event) ?? 0) + 1);
979
1059
  if (isFiniteNumber$1(sample.buildCoreMs)) buildCoreValues.push(sample.buildCoreMs);
@@ -1040,9 +1120,24 @@ async function analyzeHmrProfile(options) {
1040
1120
  profilePath: options.profilePath,
1041
1121
  sampleCount: samples.length,
1042
1122
  skippedLineCount,
1123
+ inputCoverage,
1124
+ timelines: samples.map((sample) => ({
1125
+ batchId: sample.batchId,
1126
+ completionBoundary: sample.completionBoundary,
1127
+ ...attributeHmrProfile(sample)
1128
+ })),
1043
1129
  firstTimestamp: orderedByTime[0]?.timestamp,
1044
1130
  lastTimestamp: orderedByTime[orderedByTime.length - 1]?.timestamp,
1045
1131
  metrics: {
1132
+ batchWaitMs: createMetricSummary(batchWaitValues),
1133
+ queueWaitMs: createMetricSummary(queueWaitValues),
1134
+ bundlerMs: createMetricSummary(bundlerValues),
1135
+ finalizePrepareMs: createMetricSummary(finalizePrepareValues),
1136
+ finalizeTemplateMs: createMetricSummary(finalizeTemplateValues),
1137
+ finalizePublishMs: createMetricSummary(finalizePublishValues),
1138
+ publicationValidateMs: createMetricSummary(publicationValidateValues),
1139
+ publicationIndependentMs: createMetricSummary(publicationIndependentValues),
1140
+ publicationPruneMs: createMetricSummary(publicationPruneValues),
1046
1141
  totalMs: createMetricSummary(totalValues),
1047
1142
  buildCoreMs: createMetricSummary(buildCoreValues),
1048
1143
  buildStartMs: createMetricSummary(buildStartValues),
@@ -1103,7 +1198,7 @@ async function analyzeHmrProfile(options) {
1103
1198
  //#endregion
1104
1199
  //#region src/analyze/preload/budget.ts
1105
1200
  const PRELOAD_LIMIT_BYTES = 2097152;
1106
- function createPackageSizeMap$1(result) {
1201
+ function createPackageSizeMap(result) {
1107
1202
  return new Map((result?.packages ?? []).map((pkg) => [pkg.id, pkg.files.reduce((total, file) => total + (file.size ?? 0), 0)]));
1108
1203
  }
1109
1204
  function getConfiguredPackages(rule, appJson) {
@@ -1113,7 +1208,7 @@ function getConfiguredPackages(rule, appJson) {
1113
1208
  return packages.flatMap((item) => typeof item === "string" ? [resolvePreloadPackageIdentifier(item, appJson)] : []).filter((item) => Boolean(item));
1114
1209
  }
1115
1210
  function createPreloadBudgets(appJson, result, configuredRules, packageAnalysis) {
1116
- const packageSizes = createPackageSizeMap$1(packageAnalysis);
1211
+ const packageSizes = createPackageSizeMap(packageAnalysis);
1117
1212
  const bySourcePackage = /* @__PURE__ */ new Map();
1118
1213
  const ensureSourcePackage = (page) => {
1119
1214
  const sourcePackage = page.packageRoot ?? "__main__";
@@ -1442,140 +1537,100 @@ function analyzeComponentUsage(options) {
1442
1537
  });
1443
1538
  }
1444
1539
  //#endregion
1445
- //#region src/analyze/subpackages/metadata.ts
1446
- const defaultTotalBudgetBytes = 20971520;
1447
- const defaultPackageBudgetBytes = 2097152;
1448
- const defaultWarningRatio = .85;
1449
- const defaultHistoryDir = ".weapp-vite/analyze-history";
1450
- const defaultHistoryLimit = 20;
1451
- function resolveBudgetValue(value, fallback) {
1452
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1453
- }
1454
- function resolveHistoryLimit(value) {
1455
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : defaultHistoryLimit;
1456
- }
1457
- function resolveAnalyzeBudgets(configService) {
1458
- const budgets = configService.weappViteConfig.analyze?.budgets;
1459
- const legacyPackageBudget = configService.weappViteConfig.packageSizeWarningBytes;
1460
- const packageFallback = resolveBudgetValue(legacyPackageBudget, defaultPackageBudgetBytes);
1461
- return {
1462
- totalBytes: resolveBudgetValue(budgets?.totalBytes, defaultTotalBudgetBytes),
1463
- mainBytes: resolveBudgetValue(budgets?.mainBytes, packageFallback),
1464
- subPackageBytes: resolveBudgetValue(budgets?.subPackageBytes, packageFallback),
1465
- independentBytes: resolveBudgetValue(budgets?.independentBytes, packageFallback),
1466
- warningRatio: resolveBudgetValue(budgets?.warningRatio, defaultWarningRatio),
1467
- source: budgets ? "config" : "default"
1468
- };
1469
- }
1470
- function resolveAnalyzeHistoryMetadata(configService) {
1471
- const history = configService.weappViteConfig.analyze?.history;
1472
- if (history === false) return {
1473
- enabled: false,
1474
- dir: path.resolve(configService.cwd, defaultHistoryDir),
1475
- limit: defaultHistoryLimit
1476
- };
1477
- const historyConfig = typeof history === "object" && history ? history : {};
1478
- const rawDir = typeof historyConfig.dir === "string" && historyConfig.dir.trim() ? historyConfig.dir.trim() : defaultHistoryDir;
1479
- return {
1480
- enabled: historyConfig.enabled !== false,
1481
- dir: path.isAbsolute(rawDir) ? rawDir : path.resolve(configService.cwd, rawDir),
1482
- limit: resolveHistoryLimit(historyConfig.limit)
1483
- };
1484
- }
1485
- function createAnalyzeMetadata(configService, now = /* @__PURE__ */ new Date()) {
1486
- const history = resolveAnalyzeHistoryMetadata(configService);
1487
- const packageName = configService.packageJson?.name;
1488
- return {
1489
- projectName: typeof packageName === "string" && packageName.trim() ? packageName.trim() : path.basename(configService.cwd),
1490
- generatedAt: now.toISOString(),
1491
- budgets: resolveAnalyzeBudgets(configService),
1492
- history: {
1493
- ...history,
1494
- dir: configService.relativeCwd(history.dir)
1540
+ //#region src/analyze/subpackages/artifacts/owner.ts
1541
+ /** 每次分析独立缓存包边界,兼容 pnpm、工作区和普通 node_modules。 */
1542
+ function createModuleOwnerResolver() {
1543
+ const cache = /* @__PURE__ */ new Map();
1544
+ const resolveDirectory = (directory) => {
1545
+ if (cache.has(directory)) return cache.get(directory);
1546
+ let owner;
1547
+ try {
1548
+ const value = JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8"));
1549
+ if (value && typeof value === "object" && "name" in value && typeof value.name === "string") owner = {
1550
+ name: value.name,
1551
+ version: "version" in value && typeof value.version === "string" ? value.version : void 0
1552
+ };
1553
+ } catch (error) {
1554
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) throw error;
1495
1555
  }
1556
+ const parent = path.dirname(directory);
1557
+ if (!owner && parent !== directory && path.basename(directory) !== "node_modules") owner = resolveDirectory(parent);
1558
+ cache.set(directory, owner);
1559
+ return owner;
1496
1560
  };
1561
+ return (id) => id === "\0rolldown/runtime.js" ? { name: "rolldown" } : path.isAbsolute(id) && !id.includes("\0") ? resolveDirectory(path.dirname(id.replace(/[?#].*$/, ""))) : void 0;
1562
+ }
1563
+ function classifyOwnedModule(owner, application) {
1564
+ if (application) return "application";
1565
+ if (owner?.name === "@vue/reactivity") return "reactivity";
1566
+ if (owner?.name === "@vue/shared" || owner?.name === "@weapp-core/constants" || owner?.name === "rolldown") return "helper";
1567
+ if (owner && [
1568
+ "weapi",
1569
+ "@wevu/api",
1570
+ "@wevu/web-apis",
1571
+ "@weapp-vite/web",
1572
+ "@weapp-vite/web-apis"
1573
+ ].includes(owner.name)) return "host";
1574
+ if (owner && ["wevu", "@weapp-vite/react"].includes(owner.name)) return "runtime";
1575
+ return owner ? "dependency" : "unknown";
1497
1576
  }
1498
1577
  //#endregion
1499
- //#region src/analyze/subpackages/classifier.ts
1500
- const VIRTUAL_MODULE_INDICATOR = "\0";
1501
- const VIRTUAL_PREFIX = `${SHARED_CHUNK_VIRTUAL_PREFIX}/`;
1502
- function classifyModuleSourceKind(options) {
1503
- if (options.isNodeModule) return "node_modules";
1504
- if (options.inSrc) return "src";
1505
- if (options.inPlugin) return "plugin";
1506
- return "workspace";
1507
- }
1508
- function resolvePluginAssetAbsolute(normalizedFileName, pluginRoot) {
1509
- if (!pluginRoot) return;
1510
- if (posix.isAbsolute(normalizedFileName)) return isPathInside(pluginRoot, normalizedFileName) ? normalizedFileName : void 0;
1511
- const pluginBase = posix.basename(pluginRoot);
1512
- if (normalizedFileName !== pluginBase && !normalizedFileName.startsWith(`${pluginBase}/`)) return;
1513
- const relative = normalizedFileName === pluginBase ? "" : normalizedFileName.slice(pluginBase.length + 1);
1514
- const absolute = posix.resolve(pluginRoot, relative);
1515
- return isPathInside(pluginRoot, absolute) ? absolute : void 0;
1516
- }
1517
- function resolveSubPackageRoot$1(fileName, context) {
1518
- const normalized = posix.normalize(fileName);
1519
- return Array.from(context.subPackageRoots).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => normalized === root || normalized.startsWith(`${root}/`));
1520
- }
1521
- function classifyPackage(fileName, origin, context) {
1522
- if (fileName.startsWith(VIRTUAL_PREFIX)) {
1523
- const combination = fileName.slice(VIRTUAL_PREFIX.length).split("/")[0] || "shared";
1524
- return {
1525
- id: `virtual:${combination}`,
1526
- label: `共享虚拟包 ${combination}`,
1527
- type: "virtual"
1528
- };
1529
- }
1530
- const rootCandidate = resolveSubPackageRoot$1(fileName, context);
1531
- if (rootCandidate) {
1532
- const isIndependent = context.independentRoots.has(rootCandidate);
1533
- return {
1534
- id: rootCandidate,
1535
- label: `${isIndependent ? "独立分包" : "分包"} ${rootCandidate}`,
1536
- type: isIndependent || origin === "independent" ? "independent" : "subPackage"
1537
- };
1578
+ //#region src/analyze/subpackages/artifacts/index.ts
1579
+ function createArtifactAnalysis(packages, resolveOwner = createModuleOwnerResolver()) {
1580
+ const files = /* @__PURE__ */ new Map();
1581
+ const moduleCopies = /* @__PURE__ */ new Map();
1582
+ for (const pkg of packages) for (const file of pkg.files) {
1583
+ if (files.has(file.file)) throw new Error(`产物重复归属:${file.file}`);
1584
+ if (typeof file.size !== "number" || !Number.isFinite(file.size) || file.size < 0) throw new Error(`产物缺少有效字节数:${file.file}`);
1585
+ const retained = (file.modules ?? []).filter((module) => module.bytes !== 0);
1586
+ const length = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
1587
+ const totalLength = retained.reduce((sum, module) => sum + length(module.bytes), 0);
1588
+ const scale = Math.max(totalLength, file.moduleRenderedLength ?? 0, file.size);
1589
+ const modules = retained.map((module) => {
1590
+ const owner = resolveOwner(module.id);
1591
+ const category = classifyOwnedModule(owner, module.sourceType === "src" || module.sourceType === "plugin");
1592
+ const estimatedBytes = scale > 0 ? file.size * length(module.bytes) / scale : 0;
1593
+ const copies = moduleCopies.get(module.id) ?? [];
1594
+ copies.push(estimatedBytes);
1595
+ moduleCopies.set(module.id, copies);
1596
+ return {
1597
+ source: module.source,
1598
+ package: owner,
1599
+ category,
1600
+ renderedLength: module.bytes,
1601
+ estimatedBytes
1602
+ };
1603
+ });
1604
+ const runtime = modules.filter((module) => isRuntimeCategory(module.category));
1605
+ const roles = new Set(modules.map((module) => isRuntimeCategory(module.category) ? "runtime" : module.category === "application" ? "application" : module.category === "dependency" ? "dependency" : "unknown"));
1606
+ const isAsset = file.type === "asset" && !file.modules && !/\.[cm]?js$/i.test(file.file);
1607
+ const role = isAsset ? "asset" : roles.size > 1 ? "mixed" : [...roles][0] ?? "unknown";
1608
+ files.set(file.file, {
1609
+ file: file.file,
1610
+ packageId: pkg.id,
1611
+ origin: file.from,
1612
+ type: file.type,
1613
+ bytes: file.size,
1614
+ sha256: file.sha256,
1615
+ role,
1616
+ classification: isAsset ? "asset" : modules.length ? "module-ownership" : "unavailable",
1617
+ estimation: "rendered-length-proportional",
1618
+ modules,
1619
+ runtimeEstimatedBytes: runtime.reduce((sum, module) => sum + module.estimatedBytes, 0),
1620
+ unattributedBytes: Math.max(0, file.size - modules.reduce((sum, module) => sum + module.estimatedBytes, 0))
1621
+ });
1538
1622
  }
1623
+ const artifacts = [...files.values()].sort((a, b) => a.file.localeCompare(b.file));
1624
+ if (artifacts.length === 0) throw new Error("未生成可分析产物,不能把缺失输出计为零字节。");
1539
1625
  return {
1540
- id: "__main__",
1541
- label: "主包",
1542
- type: "main"
1543
- };
1544
- }
1545
- function normalizeModuleId(id) {
1546
- if (!id || id.includes(VIRTUAL_MODULE_INDICATOR)) return;
1547
- if (!posix.isAbsolute(id)) return;
1548
- return posix.normalize(id);
1549
- }
1550
- function resolveModuleSourceType(absoluteId, ctx) {
1551
- const { configService } = ctx;
1552
- const isNodeModule = absoluteId.includes("/node_modules/") || absoluteId.includes("\\node_modules\\");
1553
- const pluginRoot = configService.absolutePluginRoot;
1554
- const srcRoot = configService.absoluteSrcRoot;
1555
- const sourceType = classifyModuleSourceKind({
1556
- isNodeModule,
1557
- inSrc: isPathInside(srcRoot, absoluteId),
1558
- inPlugin: pluginRoot ? isPathInside(pluginRoot, absoluteId) : false
1559
- });
1560
- return {
1561
- source: configService.relativeAbsoluteSrcRoot(absoluteId),
1562
- sourceType
1563
- };
1564
- }
1565
- function resolveAssetSource(fileName, ctx) {
1566
- const { configService } = ctx;
1567
- const normalized = posix.normalize(fileName);
1568
- const srcCandidate = posix.isAbsolute(normalized) ? normalized : posix.resolve(configService.absoluteSrcRoot, normalized);
1569
- if (isPathInside(configService.absoluteSrcRoot, srcCandidate)) return {
1570
- absolute: srcCandidate,
1571
- source: configService.relativeAbsoluteSrcRoot(srcCandidate),
1572
- sourceType: "src"
1573
- };
1574
- const pluginAbsolute = resolvePluginAssetAbsolute(normalized, configService.absolutePluginRoot);
1575
- if (pluginAbsolute) return {
1576
- absolute: pluginAbsolute,
1577
- source: configService.relativeAbsoluteSrcRoot(pluginAbsolute),
1578
- sourceType: "plugin"
1626
+ files: artifacts,
1627
+ totalBytes: artifacts.reduce((sum, file) => sum + file.bytes, 0),
1628
+ duplicateEstimatedBytes: [...moduleCopies.values()].reduce((sum, copies) => sum + copies.reduce((a, b) => a + b, 0) - Math.max(...copies), 0),
1629
+ runtime: {
1630
+ estimatedBytes: artifacts.reduce((sum, file) => sum + file.runtimeEstimatedBytes, 0),
1631
+ upperBoundBytes: artifacts.filter((file) => file.modules.some((module) => isRuntimeCategory(module.category))).reduce((sum, file) => sum + file.bytes, 0),
1632
+ unknownFiles: artifacts.filter((file) => file.role === "unknown" || file.modules.some((module) => module.category === "unknown")).map((file) => file.file)
1633
+ }
1579
1634
  };
1580
1635
  }
1581
1636
  //#endregion
@@ -1630,6 +1685,7 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
1630
1685
  type: "chunk",
1631
1686
  from: origin,
1632
1687
  size: typeof chunk.code === "string" ? Buffer.byteLength(chunk.code, "utf8") : void 0,
1688
+ sha256: createHash("sha256").update(chunk.code).digest("hex"),
1633
1689
  ...getCompressedSizes$1(chunk.code),
1634
1690
  isEntry: chunk.isEntry,
1635
1691
  modules: []
@@ -1637,10 +1693,15 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
1637
1693
  if (chunk.imports.length > 0) chunkEntry.imports = [...chunk.imports].sort((a, b) => a.localeCompare(b));
1638
1694
  if (chunk.dynamicImports.length > 0) chunkEntry.dynamicImports = [...chunk.dynamicImports].sort((a, b) => a.localeCompare(b));
1639
1695
  const moduleEntries = Object.entries(chunk.modules ?? {});
1696
+ chunkEntry.moduleRenderedLength = moduleEntries.reduce((sum, [, info]) => sum + (typeof info.renderedLength === "number" && Number.isFinite(info.renderedLength) && info.renderedLength > 0 ? info.renderedLength : 0), 0);
1640
1697
  for (const [rawModuleId, info] of moduleEntries) {
1641
- const absoluteId = normalizeModuleId(rawModuleId);
1698
+ const generatedRuntime = rawModuleId === "\0rolldown/runtime.js";
1699
+ const absoluteId = generatedRuntime ? rawModuleId : normalizeModuleId(rawModuleId);
1642
1700
  if (!absoluteId) continue;
1643
- const { source, sourceType } = resolveModuleSourceType(absoluteId, ctx);
1701
+ const { source, sourceType } = generatedRuntime ? {
1702
+ source: "rolldown:runtime",
1703
+ sourceType: "workspace"
1704
+ } : resolveModuleSourceType(absoluteId, ctx);
1644
1705
  const moduleEntry = {
1645
1706
  id: absoluteId,
1646
1707
  source,
@@ -1663,6 +1724,7 @@ function processAsset(asset, origin, ctx, classifierContext, packages, modules)
1663
1724
  type: "asset",
1664
1725
  from: origin,
1665
1726
  size: assetBuffer?.byteLength,
1727
+ sha256: assetBuffer ? createHash("sha256").update(assetBuffer).digest("hex") : void 0,
1666
1728
  ...getCompressedSizes$1(assetBuffer)
1667
1729
  };
1668
1730
  let assetSource;
@@ -1683,7 +1745,16 @@ function processOutput(output, origin, ctx, classifierContext, packages, modules
1683
1745
  processChunk(item, origin, ctx, classifierContext, packages, modules);
1684
1746
  onArtifact?.(item.fileName, item.code);
1685
1747
  } else if (item.type === "asset") {
1686
- processAsset(item, origin, ctx, classifierContext, packages, modules);
1748
+ const originalChunk = ctx.runtimeState?.build?.output?.analysisChunks?.get(item.fileName);
1749
+ if (originalChunk) {
1750
+ processChunk({
1751
+ ...originalChunk,
1752
+ fileName: item.fileName,
1753
+ code: Buffer.from(item.source).toString("utf8")
1754
+ }, origin, ctx, classifierContext, packages, modules);
1755
+ const classification = classifyPackage(item.fileName, origin, classifierContext);
1756
+ packages.get(classification.id).files.get(item.fileName).type = "asset";
1757
+ } else processAsset(item, origin, ctx, classifierContext, packages, modules);
1687
1758
  onArtifact?.(item.fileName, item.source);
1688
1759
  }
1689
1760
  }
@@ -1788,7 +1859,7 @@ function summarizeSubPackages(metas) {
1788
1859
  }
1789
1860
  //#endregion
1790
1861
  //#region src/analyze/subpackages/index.ts
1791
- async function analyzeSubpackages(ctx, options) {
1862
+ async function analyzeBuild(ctx, options) {
1792
1863
  const { configService, scanService, buildService } = ctx;
1793
1864
  if (!configService || !scanService || !buildService) throw new Error("analyzeSubpackages 需要先初始化 configService、scanService 和 buildService。");
1794
1865
  await scanService.loadAppEntry();
@@ -1826,9 +1897,22 @@ async function analyzeSubpackages(ctx, options) {
1826
1897
  }
1827
1898
  expandVirtualModulePlacements(modules, packages, classifierContext);
1828
1899
  const subPackages = summarizeSubPackages(subPackageMetas);
1829
- return {
1900
+ const packageReports = summarizePackages(packages);
1901
+ const artifacts = createArtifactAnalysis(packageReports);
1902
+ const result = {
1903
+ schemaVersion: 2,
1904
+ build: {
1905
+ id: createHash("sha256").update(JSON.stringify([
1906
+ configService.platform,
1907
+ configService.mode,
1908
+ artifacts.files.map((file) => [file.file, file.sha256])
1909
+ ])).digest("hex"),
1910
+ platform: configService.platform,
1911
+ mode: configService.mode
1912
+ },
1913
+ artifacts,
1830
1914
  metadata: createAnalyzeMetadata(ctx.configService),
1831
- packages: summarizePackages(packages),
1915
+ packages: packageReports,
1832
1916
  modules: summarizeModules(modules),
1833
1917
  subPackages,
1834
1918
  components: analyzeComponentUsage({
@@ -1837,6 +1921,17 @@ async function analyzeSubpackages(ctx, options) {
1837
1921
  }),
1838
1922
  glassEasel: createGlassEaselAnalyzeResult(ctx)
1839
1923
  };
1924
+ result.budgetChecks = createAnalyzeBudgetCheck(result);
1925
+ return result;
1926
+ }
1927
+ async function analyzeSubpackages(ctx, options) {
1928
+ const previous = ctx.runtimeState.build.output.analysisChunks;
1929
+ ctx.runtimeState.build.output.analysisChunks = /* @__PURE__ */ new Map();
1930
+ try {
1931
+ return await analyzeBuild(ctx, options);
1932
+ } finally {
1933
+ ctx.runtimeState.build.output.analysisChunks = previous;
1934
+ }
1840
1935
  }
1841
1936
  //#endregion
1842
1937
  //#region src/analyze/subpackages/history.ts
@@ -1908,6 +2003,7 @@ function formatAnalyzeBytes(bytes) {
1908
2003
  return `${value.toFixed(value >= 100 || unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
1909
2004
  }
1910
2005
  function formatDelta(bytes) {
2006
+ if (bytes === null) return "缺少体积测量";
1911
2007
  if (typeof bytes !== "number" || Number.isNaN(bytes) || bytes === 0) return "无变化";
1912
2008
  return `${bytes > 0 ? "+" : "-"}${formatAnalyzeBytes(Math.abs(bytes))}`;
1913
2009
  }
@@ -1917,77 +2013,8 @@ function getFileSize(file) {
1917
2013
  function getCompressedSize(file) {
1918
2014
  return file.brotliSize ?? file.gzipSize ?? 0;
1919
2015
  }
1920
- function getBudgetLimit(type, budgets) {
1921
- if (!budgets) return;
1922
- if (type === "main") return budgets.mainBytes;
1923
- if (type === "subPackage") return budgets.subPackageBytes;
1924
- if (type === "independent") return budgets.independentBytes;
1925
- }
1926
- function createPackageSizeMap(result) {
1927
- return new Map((result?.packages ?? []).map((pkg) => {
1928
- const totalBytes = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
1929
- return [pkg.id, totalBytes];
1930
- }));
1931
- }
1932
- function createFileKey(packageId, fileName) {
1933
- return `${packageId}\u0000${fileName}`;
1934
- }
1935
- function createFileSizeMap(result) {
1936
- const map = /* @__PURE__ */ new Map();
1937
- for (const pkg of result?.packages ?? []) for (const file of pkg.files) map.set(createFileKey(pkg.id, file.file), getFileSize(file));
1938
- return map;
1939
- }
1940
- function createPackageTypeMap(result) {
1941
- return new Map(result.packages.map((pkg) => [pkg.id, pkg.type]));
1942
- }
1943
- function createModuleByteMap(result) {
1944
- const map = /* @__PURE__ */ new Map();
1945
- for (const pkg of result.packages) for (const file of pkg.files) for (const mod of file.modules ?? []) {
1946
- const bytes = mod.bytes ?? mod.originalBytes ?? 0;
1947
- map.set(mod.id, Math.max(map.get(mod.id) ?? 0, bytes));
1948
- }
1949
- return map;
1950
- }
1951
- function classifyIncrementCategory(source, sourceType) {
1952
- if (source.includes("wevu") || source.includes("@weapp-vite/dashboard")) return "WeVu / runtime";
1953
- if (sourceType === "node_modules" || source.includes("node_modules")) return "第三方依赖";
1954
- if (sourceType === "workspace") return "工作区包";
1955
- if (sourceType === "plugin") return "插件生成";
1956
- if (source.endsWith(".wxss") || source.endsWith(".css") || source.endsWith(".scss")) return "样式资源";
1957
- if (source.endsWith(".wxml") || source.endsWith(".json")) return "页面结构";
1958
- return "业务源码";
1959
- }
1960
- function createIncrementAdvice(item) {
1961
- if (item.category === "第三方依赖") return "检查依赖是否只在必要分包引用,必要时收敛到公共入口或替换轻量实现。";
1962
- if (item.category === "WeVu / runtime") return "确认运行时能力是否被新页面引入,优先排查组件和 API 引用边界。";
1963
- if (item.category === "样式资源") return "检查样式重复、原子类生成范围和组件样式裁剪。";
1964
- if (item.type === "new-file" || item.type === "new-module") return "确认是否为本次需求必要新增,评估分包归属和懒加载边界。";
1965
- return "对比本次变更,优先查看新增引用、共享模块和大对象常量。";
1966
- }
1967
- function createModuleSizeMap(result) {
1968
- const map = /* @__PURE__ */ new Map();
1969
- for (const pkg of result?.packages ?? []) for (const file of pkg.files) for (const module of file.modules ?? []) {
1970
- const bytes = module.bytes ?? module.originalBytes ?? 0;
1971
- const existing = map.get(module.id);
1972
- if (existing && existing.bytes >= bytes) continue;
1973
- map.set(module.id, {
1974
- source: module.source,
1975
- sourceType: module.sourceType,
1976
- bytes,
1977
- packageLabel: pkg.label,
1978
- file: file.file
1979
- });
1980
- }
1981
- return map;
1982
- }
1983
- function createDuplicateAdvice(sourceType, packageIds, packageTypeMap, estimatedSavingBytes) {
1984
- if (packageIds.some((packageId) => packageTypeMap.get(packageId) === "independent")) return estimatedSavingBytes > 0 ? "含独立分包,先确认隔离要求,再评估是否抽公共入口。" : "含独立分包,重复可能来自隔离边界。";
1985
- if (sourceType === "node_modules") return "依赖被多个包带入,检查引用边界或考虑主包公共入口。";
1986
- if (sourceType === "src" || sourceType === "workspace") return "共享源码跨包重复,优先抽公共模块或调整分包归属。";
1987
- if (sourceType === "plugin") return "插件生成内容跨包重复,检查插件产物输出策略。";
1988
- return "检查该模块是否需要在多个包内重复存在。";
1989
- }
1990
2016
  function formatBudgetStatus(item) {
2017
+ if (item.status === "unknown") return "归因或产物字节不足,无法验收";
1991
2018
  if (item.status === "ok") return "正常";
1992
2019
  return `${item.status === "exceeded" ? "超预算" : "接近预算"} ${(item.ratio * 100).toFixed(1)}%`;
1993
2020
  }
@@ -2002,124 +2029,22 @@ function createActionItems(options) {
2002
2029
  if (actions.length === 0) actions.push("当前没有预算超限或高收益重复模块,保持观察即可。");
2003
2030
  return actions;
2004
2031
  }
2005
- function createAnalyzeBudgetCheck(result) {
2006
- const budgets = result.metadata?.budgets;
2007
- if (!budgets) return [];
2008
- const items = [];
2009
- const totalBytes = result.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
2010
- const warningRatio = budgets.warningRatio;
2011
- const createItem = (options) => {
2012
- const ratio = options.limitBytes > 0 ? options.currentBytes / options.limitBytes : 0;
2013
- const status = ratio >= 1 ? "exceeded" : ratio >= warningRatio ? "warning" : "ok";
2014
- return {
2015
- ...options,
2016
- ratio,
2017
- status
2018
- };
2019
- };
2020
- items.push(createItem({
2021
- id: "__total__",
2022
- label: "总包",
2023
- scope: "total",
2024
- currentBytes: totalBytes,
2025
- limitBytes: budgets.totalBytes
2026
- }));
2027
- for (const pkg of result.packages) {
2028
- const limitBytes = getBudgetLimit(pkg.type, budgets);
2029
- if (!limitBytes) continue;
2030
- items.push(createItem({
2031
- id: pkg.id,
2032
- label: pkg.label,
2033
- scope: pkg.type,
2034
- currentBytes: pkg.files.reduce((sum, file) => sum + getFileSize(file), 0),
2035
- limitBytes
2036
- }));
2037
- }
2038
- return items.sort((a, b) => b.ratio - a.ratio || a.label.localeCompare(b.label));
2039
- }
2040
- function createDuplicateModuleInsights(result) {
2041
- const moduleByteMap = createModuleByteMap(result);
2042
- const packageTypeMap = createPackageTypeMap(result);
2043
- return result.modules.filter((module) => module.packages.length > 1).map((module) => {
2044
- const bytes = moduleByteMap.get(module.id) ?? 0;
2045
- const packageIds = module.packages.map((pkg) => pkg.packageId);
2046
- const estimatedSavingBytes = bytes * Math.max(module.packages.length - 1, 0);
2047
- return {
2048
- id: module.id,
2049
- source: module.source,
2050
- sourceType: module.sourceType,
2051
- packageCount: module.packages.length,
2052
- bytes,
2053
- estimatedSavingBytes,
2054
- packages: packageIds,
2055
- advice: createDuplicateAdvice(module.sourceType, packageIds, packageTypeMap, estimatedSavingBytes)
2056
- };
2057
- }).sort((a, b) => b.estimatedSavingBytes - a.estimatedSavingBytes || b.packageCount - a.packageCount || a.source.localeCompare(b.source));
2058
- }
2059
- function createAnalyzeIncrementAttribution(result, previousResult) {
2060
- if (!previousResult) return [];
2061
- const previousFiles = createFileSizeMap(previousResult);
2062
- const previousModules = createModuleSizeMap(previousResult);
2063
- const currentModules = createModuleSizeMap(result);
2064
- const items = [];
2065
- for (const pkg of result.packages) for (const file of pkg.files) {
2066
- const currentBytes = getFileSize(file);
2067
- const previousBytes = previousFiles.get(createFileKey(pkg.id, file.file)) ?? 0;
2068
- const deltaBytes = currentBytes - previousBytes;
2069
- if (deltaBytes <= 0) continue;
2070
- const type = previousBytes > 0 ? "increased-file" : "new-file";
2071
- const item = {
2072
- key: `file:${pkg.id}:${file.file}`,
2073
- type,
2074
- label: file.file,
2075
- category: classifyIncrementCategory(file.source ?? file.file),
2076
- packageLabel: pkg.label,
2077
- file: file.file,
2078
- currentBytes,
2079
- previousBytes,
2080
- deltaBytes,
2081
- advice: ""
2082
- };
2083
- items.push({
2084
- ...item,
2085
- advice: createIncrementAdvice(item)
2086
- });
2087
- }
2088
- for (const [id, module] of currentModules) {
2089
- const previousBytes = previousModules.get(id)?.bytes ?? 0;
2090
- const deltaBytes = module.bytes - previousBytes;
2091
- if (deltaBytes <= 0) continue;
2092
- const type = previousBytes > 0 ? "increased-module" : "new-module";
2093
- const item = {
2094
- key: `module:${id}`,
2095
- type,
2096
- label: module.source,
2097
- category: classifyIncrementCategory(module.source, module.sourceType),
2098
- packageLabel: module.packageLabel,
2099
- file: module.file,
2100
- currentBytes: module.bytes,
2101
- previousBytes,
2102
- deltaBytes,
2103
- advice: ""
2104
- };
2105
- items.push({
2106
- ...item,
2107
- advice: createIncrementAdvice(item)
2108
- });
2109
- }
2110
- return items.sort((a, b) => b.deltaBytes - a.deltaBytes || a.category.localeCompare(b.category) || a.label.localeCompare(b.label));
2032
+ function createReportGrowth(comparison) {
2033
+ if (!comparison) return [];
2034
+ return [...comparison.files, ...comparison.modules].filter((item) => item.deltaBytes !== null && item.deltaBytes > 0).sort((a, b) => b.deltaBytes - a.deltaBytes || (a.category ?? "").localeCompare(b.category ?? "") || a.label.localeCompare(b.label));
2111
2035
  }
2112
2036
  function createAnalyzeIncrementCategorySummary(items) {
2113
2037
  const map = /* @__PURE__ */ new Map();
2114
2038
  for (const item of items) {
2115
- const entry = map.get(item.category) ?? {
2116
- category: item.category,
2039
+ const category = item.category ?? "";
2040
+ const entry = map.get(category) ?? {
2041
+ category,
2117
2042
  count: 0,
2118
2043
  deltaBytes: 0
2119
2044
  };
2120
2045
  entry.count += 1;
2121
2046
  entry.deltaBytes += item.deltaBytes;
2122
- map.set(item.category, entry);
2047
+ map.set(category, entry);
2123
2048
  }
2124
2049
  return [...map.values()].sort((a, b) => b.deltaBytes - a.deltaBytes || b.count - a.count || a.category.localeCompare(b.category));
2125
2050
  }
@@ -2130,8 +2055,8 @@ function createAnalyzePrMarkdownReport(result, previousResult) {
2130
2055
  })));
2131
2056
  const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
2132
2057
  const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
2133
- const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
2134
- const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
2058
+ const comparison = previousResult ? createAnalyzeComparison(result, previousResult) : void 0;
2059
+ const incrementItems = createReportGrowth(comparison);
2135
2060
  const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
2136
2061
  const duplicateInsights = createDuplicateModuleInsights(result);
2137
2062
  const budgetItems = createAnalyzeBudgetCheck(result);
@@ -2147,7 +2072,7 @@ function createAnalyzePrMarkdownReport(result, previousResult) {
2147
2072
  return [
2148
2073
  "## weapp-vite analyze PR 摘要",
2149
2074
  "",
2150
- `- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)})`,
2075
+ `- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(comparison?.deltaBytes)})`,
2151
2076
  `- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
2152
2077
  `- 预算告警:${budgetIssues.length}`,
2153
2078
  `- 增量归因:${incrementItems.length > 0 ? `${incrementItems.length} 项正向增长` : "无正向增长"}`,
@@ -2191,11 +2116,11 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2191
2116
  const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
2192
2117
  const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
2193
2118
  const duplicateInsights = createDuplicateModuleInsights(result);
2194
- const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
2119
+ const comparison = previousResult ? createAnalyzeComparison(result, previousResult) : void 0;
2120
+ const incrementItems = createReportGrowth(comparison);
2195
2121
  const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
2196
2122
  const budgetItems = createAnalyzeBudgetCheck(result);
2197
- const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
2198
- const previousPackageSizes = createPackageSizeMap(previousResult);
2123
+ const packageDeltas = new Map(comparison?.packages.map((item) => [item.packageId, item.deltaBytes]));
2199
2124
  const budgets = result.metadata?.budgets;
2200
2125
  const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
2201
2126
  const actionItems = createActionItems({
@@ -2205,9 +2130,9 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2205
2130
  const packageRows = result.packages.map((pkg) => {
2206
2131
  const size = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
2207
2132
  const compressed = pkg.files.reduce((sum, file) => sum + getCompressedSize(file), 0);
2208
- const previousSize = previousPackageSizes.get(pkg.id);
2133
+ const delta = packageDeltas.has(pkg.id) ? packageDeltas.get(pkg.id) : 0;
2209
2134
  const budgetStatus = budgetItems.find((item) => item.id === pkg.id);
2210
- return `| ${pkg.label} | ${pkg.type} | ${formatAnalyzeBytes(size)} | ${formatAnalyzeBytes(compressed)} | ${formatDelta(typeof previousSize === "number" ? size - previousSize : void 0)} | ${budgetStatus ? formatBudgetStatus(budgetStatus) : "正常"} |`;
2135
+ return `| ${pkg.label} | ${pkg.type} | ${formatAnalyzeBytes(size)} | ${formatAnalyzeBytes(compressed)} | ${formatDelta(delta)} | ${budgetStatus ? formatBudgetStatus(budgetStatus) : "正常"} |`;
2211
2136
  }).join("\n");
2212
2137
  const topFileRows = files.sort((a, b) => getFileSize(b.file) - getFileSize(a.file) || a.file.file.localeCompare(b.file.file)).slice(0, 10).map((item) => `| ${item.file.file} | ${item.pkg.label} | ${item.file.type} | ${formatAnalyzeBytes(getFileSize(item.file))} | ${formatAnalyzeBytes(getCompressedSize(item.file))} |`).join("\n");
2213
2138
  const duplicateRows = duplicateInsights.slice(0, 10).map((module) => `| ${module.source} | ${module.sourceType} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
@@ -2223,7 +2148,7 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2223
2148
  "",
2224
2149
  `- 总产物体积:${formatAnalyzeBytes(totalBytes)}`,
2225
2150
  `- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
2226
- `- 较上次:${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)}`,
2151
+ `- 较上次:${formatDelta(comparison?.deltaBytes)}`,
2227
2152
  `- 包体数量:${result.packages.length}`,
2228
2153
  `- 源码模块:${result.modules.length}`,
2229
2154
  `- 跨包复用:${duplicateInsights.length}`,
@@ -2270,95 +2195,6 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2270
2195
  ].join("\n");
2271
2196
  }
2272
2197
  //#endregion
2273
- //#region src/cli/analyze/dashboardPolicy.ts
2274
- const rejectDashboardSharedStateSet = defineRpcFunction({
2275
- name: "devframe:rpc:server-state:set",
2276
- type: "action",
2277
- handler: () => {
2278
- throw new Error("Dashboard Devframe shared state 只允许服务端修改。");
2279
- }
2280
- });
2281
- const rejectDashboardSharedStatePatch = defineRpcFunction({
2282
- name: "devframe:rpc:server-state:patch",
2283
- type: "action",
2284
- handler: () => {
2285
- throw new Error("Dashboard Devframe shared state 只允许服务端修改。");
2286
- }
2287
- });
2288
- /** 独立宿主在开放 RPC 前禁止全局共享状态写入,不影响嵌入宿主的策略。 */
2289
- function withStandaloneDashboardPolicy(definition) {
2290
- return {
2291
- ...definition,
2292
- async setup(ctx, info) {
2293
- ctx.rpc.register(rejectDashboardSharedStateSet, true);
2294
- ctx.rpc.register(rejectDashboardSharedStatePatch, true);
2295
- await definition.setup?.(ctx, info);
2296
- }
2297
- };
2298
- }
2299
- //#endregion
2300
- //#region src/cli/analyze/dashboardViteBridge.ts
2301
- const ANALYZE_DASHBOARD_DEVFRAME_BASE = "/__weapp-vite/";
2302
- const transportPaths = /* @__PURE__ */ new Set([
2303
- `${ANALYZE_DASHBOARD_DEVFRAME_BASE}${DEVFRAME_CONNECTION_META_FILENAME}`,
2304
- `${ANALYZE_DASHBOARD_DEVFRAME_BASE}${DEVFRAME_SSE_ROUTE}`,
2305
- `${ANALYZE_DASHBOARD_DEVFRAME_BASE}${DEVFRAME_WS_ROUTE}`
2306
- ]);
2307
- function createAnalyzeDashboardViteBridge(controller) {
2308
- let instance;
2309
- let closing;
2310
- const closeInstance = async () => {
2311
- controller.dispose();
2312
- if (closing) return await closing;
2313
- const current = instance;
2314
- instance = void 0;
2315
- if (!current) return;
2316
- closing = current.close().finally(() => {
2317
- closing = void 0;
2318
- });
2319
- await closing;
2320
- };
2321
- return {
2322
- name: "weapp-vite:dashboard-devframe",
2323
- apply: "serve",
2324
- async configureServer(server) {
2325
- const httpServer = server.httpServer instanceof Server ? server.httpServer : void 0;
2326
- const created = initDevframe(withStandaloneDashboardPolicy(controller.definition), {
2327
- base: ANALYZE_DASHBOARD_DEVFRAME_BASE,
2328
- distDir: false,
2329
- ...httpServer ? { server: httpServer } : {
2330
- host: "127.0.0.1",
2331
- ws: { sidecar: true }
2332
- },
2333
- allowedOrigins: [],
2334
- auth: true,
2335
- mcp: false
2336
- });
2337
- try {
2338
- server.middlewares.use((request, response, next) => {
2339
- const url = request.url ?? "/";
2340
- const queryIndex = url.indexOf("?");
2341
- const pathname = queryIndex < 0 ? url : url.slice(0, queryIndex);
2342
- if (transportPaths.has(pathname)) created.nodeMiddleware(request, response, next);
2343
- else next();
2344
- });
2345
- await created.ready;
2346
- instance = created;
2347
- } catch (error) {
2348
- controller.dispose();
2349
- await created.close().catch(() => {});
2350
- throw error;
2351
- }
2352
- server.httpServer?.once("close", () => {
2353
- closeInstance();
2354
- });
2355
- },
2356
- async closeBundle() {
2357
- await closeInstance();
2358
- }
2359
- };
2360
- }
2361
- //#endregion
2362
2198
  //#region src/cli/analyze/dashboard.ts
2363
2199
  function createInstallCommand(agent) {
2364
2200
  const resolved = resolveCommand(agent ?? "npm", "install", [ANALYZE_DASHBOARD_PACKAGE_NAME]);
@@ -2366,6 +2202,7 @@ function createInstallCommand(agent) {
2366
2202
  return `${resolved.command} ${resolved.args.join(" ")}`;
2367
2203
  }
2368
2204
  async function startAnalyzeDashboard(result, options) {
2205
+ const shutdown = getDevShutdownScope();
2369
2206
  const resolved = resolveDashboardRoot(options);
2370
2207
  if (!resolved) {
2371
2208
  logger_default.warn(`[weapp-vite ui] 未安装可选仪表盘包 ${colors.bold(colors.green(ANALYZE_DASHBOARD_PACKAGE_NAME))},已自动降级关闭 dashboard 能力。`);
@@ -2373,7 +2210,9 @@ async function startAnalyzeDashboard(result, options) {
2373
2210
  return;
2374
2211
  }
2375
2212
  const { root, configFile } = resolved;
2213
+ const { ANALYZE_DASHBOARD_DEVFRAME_BASE, ANALYZE_DASHBOARD_HUB_BASE, createAnalyzeDashboardViteBridge } = await import("./dashboardViteBridge-Dy3nAD4m.mjs");
2376
2214
  const devframe = createAnalyzeDashboardDevframe({
2215
+ clientAssets: options.uiHost === "hub" ? root : void 0,
2377
2216
  snapshot: {
2378
2217
  current: result,
2379
2218
  previous: options.previousResult ?? null,
@@ -2392,9 +2231,21 @@ async function startAnalyzeDashboard(result, options) {
2392
2231
  srcRoot: options.srcRoot ?? (options.cwd ? path.resolve(options.cwd, "src") : void 0)
2393
2232
  }
2394
2233
  });
2234
+ const bridge = createAnalyzeDashboardViteBridge(devframe, {
2235
+ projectRoot: options.cwd ?? process.cwd(),
2236
+ uiHost: options.uiHost
2237
+ });
2395
2238
  let server;
2239
+ let onHostClose;
2240
+ const closeResources = async () => {
2241
+ return (await Promise.allSettled([
2242
+ Promise.resolve().then(() => devframe.dispose()),
2243
+ Promise.resolve().then(() => bridge.close()),
2244
+ Promise.resolve().then(() => shutdown ? shutdown.run("cleanup", () => server?.close()) : server?.close())
2245
+ ])).filter((result) => result.status === "rejected").map((result) => result.reason);
2246
+ };
2396
2247
  try {
2397
- server = await createServer({
2248
+ server = await createDevViteServer({
2398
2249
  root,
2399
2250
  base: ANALYZE_DASHBOARD_DEVFRAME_BASE,
2400
2251
  configFile: configFile ?? false,
@@ -2405,9 +2256,18 @@ async function startAnalyzeDashboard(result, options) {
2405
2256
  name: "weapp-vite:dashboard-lifetime",
2406
2257
  enforce: "pre",
2407
2258
  configureServer(createdServer) {
2408
- server = createdServer;
2259
+ server ??= createdServer;
2260
+ const record = getDevServerCloseRecord(createdServer);
2261
+ const close = record.close;
2262
+ replaceDevServerClose(createdServer, async () => {
2263
+ try {
2264
+ await close();
2265
+ } finally {
2266
+ if (server && getDevServerCloseRecord(server) === record) onHostClose?.();
2267
+ }
2268
+ });
2409
2269
  }
2410
- }, createAnalyzeDashboardViteBridge(devframe)],
2270
+ }, bridge],
2411
2271
  server: {
2412
2272
  host: "127.0.0.1",
2413
2273
  port: 0,
@@ -2415,16 +2275,19 @@ async function startAnalyzeDashboard(result, options) {
2415
2275
  },
2416
2276
  logLevel: "error"
2417
2277
  });
2418
- await server.listen(0);
2278
+ if (!shutdown?.stopping) await server.listen(0);
2419
2279
  } catch (error) {
2420
- devframe.dispose();
2421
- await server?.close().catch((closeError) => logger_default.error(closeError));
2280
+ const cleanupErrors = await closeResources();
2281
+ if (cleanupErrors.length) throw new AggregateError([error, ...cleanupErrors], "Dashboard 启动及资源清理失败", { cause: error });
2422
2282
  throw error;
2423
2283
  }
2424
2284
  const activeServer = server;
2425
2285
  const urls = [...activeServer.resolvedUrls?.local ?? [], ...activeServer.resolvedUrls?.network ?? []];
2426
2286
  const authCode = refreshTempAuthCode();
2427
- const authenticatedUrls = urls.map((url) => buildOtpAuthUrl(url, authCode));
2287
+ const authenticatedUrls = urls.map((url) => {
2288
+ const entryUrl = options.uiHost === "hub" ? new URL(ANALYZE_DASHBOARD_HUB_BASE, url).href : url;
2289
+ return buildOtpAuthUrl(entryUrl, authCode);
2290
+ });
2428
2291
  let closing;
2429
2292
  let resolveExit;
2430
2293
  const waitPromise = new Promise((resolve) => {
@@ -2432,22 +2295,26 @@ async function startAnalyzeDashboard(result, options) {
2432
2295
  });
2433
2296
  const signals = ["SIGINT", "SIGTERM"];
2434
2297
  let onExit;
2298
+ let releaseOwnership;
2299
+ const finish = () => {
2300
+ for (const signal of signals) process.removeListener(signal, onExit);
2301
+ releaseOwnership?.();
2302
+ resolveExit();
2303
+ };
2304
+ onHostClose = () => {
2305
+ if (!closing) finish();
2306
+ };
2435
2307
  const close = () => {
2436
- if (!closing) {
2437
- devframe.dispose();
2438
- closing = Promise.resolve().then(() => activeServer.close()).finally(() => {
2439
- for (const signal of signals) process.removeListener(signal, onExit);
2440
- activeServer.httpServer?.removeListener("close", onExit);
2441
- resolveExit();
2442
- });
2443
- }
2308
+ if (!closing) closing = closeResources().then((errors) => {
2309
+ if (errors.length === 1) throw errors[0];
2310
+ if (errors.length > 1) throw new AggregateError(errors, "Dashboard 资源清理失败");
2311
+ }).finally(finish);
2444
2312
  return closing;
2445
2313
  };
2446
2314
  onExit = async () => {
2447
2315
  await close().catch((error) => logger_default.error(error));
2448
2316
  };
2449
- for (const signal of signals) process.once(signal, onExit);
2450
- activeServer.httpServer?.once("close", onExit);
2317
+ if (!shutdown) for (const signal of signals) process.once(signal, onExit);
2451
2318
  const handle = {
2452
2319
  update: devframe.update,
2453
2320
  emitRuntimeEvents: devframe.emitRuntimeEvents,
@@ -2455,6 +2322,11 @@ async function startAnalyzeDashboard(result, options) {
2455
2322
  close,
2456
2323
  urls: authenticatedUrls
2457
2324
  };
2325
+ releaseOwnership = shutdown?.own(close);
2326
+ if (shutdown?.stopping) {
2327
+ await close();
2328
+ return;
2329
+ }
2458
2330
  if (options.watch) {
2459
2331
  if (!options.silentStartupLog) {
2460
2332
  logger_default.info("weapp-vite UI 已启动(分析视图,实时模式),按 Ctrl+C 退出。");
@@ -2469,21 +2341,17 @@ async function startAnalyzeDashboard(result, options) {
2469
2341
  await waitPromise;
2470
2342
  }
2471
2343
  //#endregion
2472
- //#region src/cli/processCleanup.ts
2473
- function isSassEmbeddedChild(handle) {
2474
- if (!handle || typeof handle !== "object") return false;
2475
- const child = handle;
2476
- const spawnfile = typeof child.spawnfile === "string" ? child.spawnfile : "";
2477
- const spawnargs = Array.isArray(child.spawnargs) ? child.spawnargs : [];
2478
- return Boolean("kill" in handle && "spawnfile" in handle && (spawnfile.includes("sass-embedded") || spawnfile.includes("dart-sass") && spawnargs.includes("--embedded")));
2479
- }
2480
- function terminateStaleSassEmbeddedProcess() {
2481
- const getHandles = process._getActiveHandles;
2482
- const handles = typeof getHandles === "function" ? getHandles() : void 0;
2483
- if (!Array.isArray(handles)) return;
2484
- for (const handle of handles) if (isSassEmbeddedChild(handle)) try {
2485
- handle.kill();
2486
- } catch {}
2344
+ //#region src/cli/analyze/output.ts
2345
+ /** CLI 单次分析期间保留 JSON 专用写入入口,配置、插件和清理日志走 stderr。 */
2346
+ async function withAnalyzeOutput(json, run) {
2347
+ const stdoutWrite = process.stdout.write;
2348
+ const writeJson = (value) => stdoutWrite.call(process.stdout, `${JSON.stringify(value, null, 2)}\n`);
2349
+ if (json) process.stdout.write = process.stderr.write.bind(process.stderr);
2350
+ try {
2351
+ await run(writeJson);
2352
+ } finally {
2353
+ process.stdout.write = stdoutWrite;
2354
+ }
2487
2355
  }
2488
2356
  //#endregion
2489
2357
  //#region src/cli/commands/analyze.ts
@@ -2578,13 +2446,17 @@ function printAnalysisSummary(result) {
2578
2446
  if (duplicates.length > limit) logger_default.info(`- …其余 ${duplicates.length - limit} 项请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
2579
2447
  }
2580
2448
  function printBudgetCheckSummary(result) {
2581
- const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded");
2449
+ const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded" || item.status === "unknown");
2582
2450
  if (exceededItems.length === 0) {
2583
2451
  logger_default.success("包体预算检查通过");
2584
2452
  return false;
2585
2453
  }
2586
2454
  logger_default.error(`包体预算检查失败:${exceededItems.length} 项超限`);
2587
- for (const item of exceededItems) logger_default.error(`- ${item.label}:${formatAnalyzeBytes(item.currentBytes)} / ${formatAnalyzeBytes(item.limitBytes)} (${(item.ratio * 100).toFixed(1)}%)`);
2455
+ for (const item of exceededItems) {
2456
+ logger_default.error(`- ${item.label}:${formatAnalyzeBytes(item.currentBytes)} / ${formatAnalyzeBytes(item.limitBytes)} (${(item.ratio * 100).toFixed(1)}%)`);
2457
+ if (item.status === "unknown") logger_default.error(" 产物字节或 runtime 归因不足,无法验收预算。");
2458
+ logger_default.error(` 文件:${item.files.join(", ") || "未知"}`);
2459
+ }
2588
2460
  return true;
2589
2461
  }
2590
2462
  function printGlassEaselCheckSummary(result) {
@@ -2760,107 +2632,111 @@ function printHmrProfileAnalysisSummary(result, configService) {
2760
2632
  }
2761
2633
  }
2762
2634
  function registerAnalyzeCommand(cli) {
2763
- cli.command("analyze [root]", "analyze 两端包体与源码映射").option("--hmr-profile [file]", `[string | boolean] 分析 HMR JSONL profile,省略值时优先读取配置,否则回退到默认路径`).option("--json", `[boolean] 输出 JSON 结果`).option("--markdown", `[boolean] 输出 Markdown 报告`).option("--report <type>", `[string] 输出指定报告类型(pr)`).option("--budget-check", `[boolean] 检查 analyze 预算,超过预算时返回非 0 退出码`).option("--glass-easel-check", `[boolean] 检查 glass-easel 配置、模板和 SelectorQuery 迁移兼容性`).option("--preload", `[boolean] 分析静态页面跳转、实际分包体积和 preloadRule 额度`).option("--output <file>", `[string] 将分析结果写入指定文件(JSON 或 Markdown)`).option("-p, --platform <platform>", `[string] target platform (weapp | web)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).action(async (root, options) => {
2635
+ cli.command("analyze [root]", "analyze 两端包体与源码映射").option("--ui-host <host>", `[string] 启动调试 UI 并选择宿主(standalone | hub)`).option("--hmr-profile [file]", `[string | boolean] 分析 HMR JSONL profile,省略值时优先读取配置,否则回退到默认路径`).option("--json", `[boolean] 输出 JSON 结果`).option("--markdown", `[boolean] 输出 Markdown 报告`).option("--report <type>", `[string] 输出指定报告类型(pr)`).option("--budget-check", `[boolean] 检查 analyze 预算,超过预算时返回非 0 退出码`).option("--glass-easel-check", `[boolean] 检查 glass-easel 配置、模板和 SelectorQuery 迁移兼容性`).option("--preload", `[boolean] 分析静态页面跳转、实际分包体积和 preloadRule 额度`).option("--output <file>", `[string] 将分析结果写入指定文件(JSON 或 Markdown)`).option("-p, --platform <platform>", `[string] target platform (weapp | web)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).action(async (root, options) => {
2764
2636
  filterDuplicateOptions(options);
2637
+ options.uiHost = parseDashboardUiHost(options.uiHost);
2765
2638
  const configFile = resolveConfigFile(options);
2766
2639
  const outputJson = coerceBooleanOption(options.json);
2767
- const outputMarkdown = coerceBooleanOption(options.markdown);
2768
- const reportType = typeof options.report === "string" ? options.report.trim() : "";
2769
- const outputPrReport = reportType === "pr";
2770
- if (reportType && !outputPrReport) throw new Error(`不支持的 analyze report 类型:${reportType}`);
2771
- const budgetCheck = coerceBooleanOption(options.budgetCheck);
2772
- const glassEaselCheck = coerceBooleanOption(options.glassEaselCheck);
2773
- const launchDashboard = !outputJson && !outputMarkdown && !outputPrReport && !budgetCheck && !glassEaselCheck;
2774
- const targets = resolveRuntimeTargets(options);
2775
- const inlineConfig = createInlineConfig(targets);
2776
- let ctx;
2777
- try {
2778
- ctx = await createCompilerContext({
2779
- cwd: root,
2780
- mode: options.mode ?? "production",
2781
- configFile,
2782
- inlineConfig,
2783
- cliPlatform: targets.rawPlatform,
2784
- projectConfigPath: options.projectConfig
2785
- });
2786
- logRuntimeTarget(targets, {
2787
- silent: outputJson || outputMarkdown,
2788
- resolvedConfigPlatform: ctx.configService.platform
2789
- });
2790
- const outputOption = typeof options.output === "string" ? options.output.trim() : "";
2791
- if (options.hmrProfile !== void 0 && options.hmrProfile !== false) {
2792
- const profileOption = typeof options.hmrProfile === "string" && options.hmrProfile.trim() ? options.hmrProfile.trim() : ctx.configService.weappViteConfig.hmr?.profileJson;
2793
- const profilePath = resolveHmrProfileJsonPath({
2794
- cwd: ctx.configService.cwd,
2795
- option: profileOption,
2796
- fallbackToDefault: true
2640
+ return withAnalyzeOutput(Boolean(outputJson), async (writeJson) => {
2641
+ const outputMarkdown = coerceBooleanOption(options.markdown);
2642
+ const reportType = typeof options.report === "string" ? options.report.trim() : "";
2643
+ const outputPrReport = reportType === "pr";
2644
+ if (reportType && !outputPrReport) throw new Error(`不支持的 analyze report 类型:${reportType}`);
2645
+ const budgetCheck = coerceBooleanOption(options.budgetCheck);
2646
+ const glassEaselCheck = coerceBooleanOption(options.glassEaselCheck);
2647
+ const launchDashboard = !outputJson && !outputMarkdown && !outputPrReport && !budgetCheck && !glassEaselCheck;
2648
+ const targets = resolveRuntimeTargets(options);
2649
+ const inlineConfig = createInlineConfig(targets);
2650
+ let ctx;
2651
+ try {
2652
+ ctx = await createCompilerContext({
2653
+ cwd: root,
2654
+ mode: options.mode ?? "production",
2655
+ configFile,
2656
+ inlineConfig,
2657
+ cliPlatform: targets.rawPlatform,
2658
+ projectConfigPath: options.projectConfig
2797
2659
  });
2798
- if (!profilePath) throw new Error("未找到可用的 HMR profile 文件路径");
2799
- const hmrProfileResult = await analyzeHmrProfile({ profilePath });
2800
- const writtenPath = await writeAnalyzeResult(hmrProfileResult, outputOption, ctx.configService);
2801
- if (outputJson) {
2802
- if (!writtenPath) process.stdout.write(`${JSON.stringify(hmrProfileResult, null, 2)}\n`);
2803
- } else printHmrProfileAnalysisSummary(hmrProfileResult, ctx.configService);
2804
- return;
2805
- }
2806
- if (coerceBooleanOption(options.preload)) {
2807
- if (targets.kind !== "miniprogram" || ctx.configService.platform !== "weapp") throw new Error("preloadRule 分析目前仅支持微信小程序平台。");
2808
- const packageAnalysis = await analyzeSubpackages(ctx);
2809
- const preloadResult = await analyzePreloadRules(ctx, { packageAnalysis });
2810
- const writtenPath = await writeAnalyzeResult(preloadResult, outputOption, ctx.configService);
2811
- if (outputJson && !writtenPath) process.stdout.write(`${JSON.stringify(preloadResult, null, 2)}\n`);
2812
- if (!outputJson && !writtenPath) printPreloadAnalysisSummary(preloadResult);
2813
- return;
2814
- }
2815
- if (getBackendForCapability(targets, "web", "analyze")) {
2816
- const webResult = createWebAnalyzeResult(ctx.configService, { platform: "web" });
2817
- const writtenPath = await writeAnalyzeResult(webResult, outputOption, ctx.configService);
2818
- if (outputJson) {
2819
- if (!writtenPath) process.stdout.write(`${JSON.stringify(webResult, null, 2)}\n`);
2820
- } else printWebAnalysisSummary(webResult);
2821
- return;
2822
- }
2823
- if (!getBackendForCapability(targets, "miniprogram", "analyze")) {
2824
- logger_default.warn("当前命令不支持该平台,请通过 --platform weapp 或 --platform web 指定目标。");
2825
- return;
2826
- }
2827
- const previousResult = await readLatestAnalyzeHistorySnapshot(ctx.configService);
2828
- ctx.runtimeState.glassEasel.silent = Boolean(outputJson || outputMarkdown || outputPrReport || glassEaselCheck);
2829
- const artifactSnapshot = launchDashboard ? createDashboardArtifactSnapshot() : void 0;
2830
- const result = artifactSnapshot ? await analyzeSubpackages(ctx, { onArtifact: artifactSnapshot.capture }) : await analyzeSubpackages(ctx);
2831
- await writeAnalyzeHistorySnapshot(result, ctx.configService);
2832
- const writtenPath = await writeAnalyzeResult(result, outputOption, ctx.configService, outputPrReport ? "pr" : outputMarkdown ? "markdown" : "json", previousResult);
2833
- if (outputPrReport) {
2834
- if (!writtenPath) process.stdout.write(`${createAnalyzePrMarkdownReport(result, previousResult)}\n`);
2835
- } else if (outputMarkdown) {
2836
- if (!writtenPath) process.stdout.write(`${createAnalyzeMarkdownReport(result, previousResult)}\n`);
2837
- } else if (outputJson) {
2838
- if (!writtenPath) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
2839
- }
2840
- const budgetFailed = budgetCheck ? printBudgetCheckSummary(result) : false;
2841
- const glassEaselFailed = glassEaselCheck && !outputJson ? printGlassEaselCheckSummary(result) : glassEaselCheck && result.glassEasel.summary.errors > 0;
2842
- if (budgetFailed || glassEaselFailed) process.exitCode = 1;
2843
- if (budgetCheck || glassEaselCheck) return;
2844
- if (artifactSnapshot) {
2845
- printAnalysisSummary(result);
2846
- await startAnalyzeDashboard(result, {
2847
- artifacts: artifactSnapshot.files,
2848
- cwd: ctx.configService.cwd,
2849
- packageManagerAgent: ctx.configService.packageManager.agent,
2850
- pluginRoot: ctx.configService.absolutePluginRoot,
2851
- srcRoot: ctx.configService.absoluteSrcRoot,
2852
- previousResult
2660
+ logRuntimeTarget(targets, {
2661
+ silent: outputJson || outputMarkdown,
2662
+ resolvedConfigPlatform: ctx.configService.platform
2853
2663
  });
2664
+ const outputOption = typeof options.output === "string" ? options.output.trim() : "";
2665
+ if (options.hmrProfile !== void 0 && options.hmrProfile !== false) {
2666
+ const profileOption = typeof options.hmrProfile === "string" && options.hmrProfile.trim() ? options.hmrProfile.trim() : ctx.configService.weappViteConfig.hmr?.profileJson;
2667
+ const profilePath = resolveHmrProfileJsonPath({
2668
+ cwd: ctx.configService.cwd,
2669
+ option: profileOption,
2670
+ fallbackToDefault: true
2671
+ });
2672
+ if (!profilePath) throw new Error("未找到可用的 HMR profile 文件路径");
2673
+ const hmrProfileResult = await analyzeHmrProfile({ profilePath });
2674
+ const writtenPath = await writeAnalyzeResult(hmrProfileResult, outputOption, ctx.configService);
2675
+ if (outputJson) {
2676
+ if (!writtenPath) writeJson(hmrProfileResult);
2677
+ } else printHmrProfileAnalysisSummary(hmrProfileResult, ctx.configService);
2678
+ return;
2679
+ }
2680
+ if (coerceBooleanOption(options.preload)) {
2681
+ if (targets.kind !== "miniprogram" || ctx.configService.platform !== "weapp") throw new Error("preloadRule 分析目前仅支持微信小程序平台。");
2682
+ const packageAnalysis = await analyzeSubpackages(ctx);
2683
+ const preloadResult = await analyzePreloadRules(ctx, { packageAnalysis });
2684
+ const writtenPath = await writeAnalyzeResult(preloadResult, outputOption, ctx.configService);
2685
+ if (outputJson && !writtenPath) writeJson(preloadResult);
2686
+ if (!outputJson && !writtenPath) printPreloadAnalysisSummary(preloadResult);
2687
+ return;
2688
+ }
2689
+ if (getBackendForCapability(targets, "web", "analyze")) {
2690
+ const webResult = createWebAnalyzeResult(ctx.configService, { platform: "web" });
2691
+ const writtenPath = await writeAnalyzeResult(webResult, outputOption, ctx.configService);
2692
+ if (outputJson) {
2693
+ if (!writtenPath) writeJson(webResult);
2694
+ } else printWebAnalysisSummary(webResult);
2695
+ return;
2696
+ }
2697
+ if (!getBackendForCapability(targets, "miniprogram", "analyze")) {
2698
+ logger_default.warn("当前命令不支持该平台,请通过 --platform weapp 或 --platform web 指定目标。");
2699
+ return;
2700
+ }
2701
+ const previousResult = await readLatestAnalyzeHistorySnapshot(ctx.configService);
2702
+ ctx.runtimeState.glassEasel.silent = Boolean(outputJson || outputMarkdown || outputPrReport || glassEaselCheck);
2703
+ const artifactSnapshot = launchDashboard ? createDashboardArtifactSnapshot() : void 0;
2704
+ const result = artifactSnapshot ? await analyzeSubpackages(ctx, { onArtifact: artifactSnapshot.capture }) : await analyzeSubpackages(ctx);
2705
+ await writeAnalyzeHistorySnapshot(result, ctx.configService);
2706
+ const writtenPath = await writeAnalyzeResult(result, outputOption, ctx.configService, outputPrReport ? "pr" : outputMarkdown ? "markdown" : "json", previousResult);
2707
+ if (outputPrReport) {
2708
+ if (!writtenPath) process.stdout.write(`${createAnalyzePrMarkdownReport(result, previousResult)}\n`);
2709
+ } else if (outputMarkdown) {
2710
+ if (!writtenPath) process.stdout.write(`${createAnalyzeMarkdownReport(result, previousResult)}\n`);
2711
+ } else if (outputJson) {
2712
+ if (!writtenPath) writeJson(result);
2713
+ }
2714
+ const budgetFailed = budgetCheck ? outputJson ? createAnalyzeBudgetCheck(result).some((item) => item.status === "exceeded" || item.status === "unknown") : printBudgetCheckSummary(result) : false;
2715
+ const glassEaselFailed = glassEaselCheck && !outputJson ? printGlassEaselCheckSummary(result) : glassEaselCheck && result.glassEasel.summary.errors > 0;
2716
+ if (budgetFailed || glassEaselFailed) process.exitCode = 1;
2717
+ if (budgetCheck || glassEaselCheck) return;
2718
+ if (artifactSnapshot) {
2719
+ printAnalysisSummary(result);
2720
+ await startAnalyzeDashboard(result, {
2721
+ uiHost: options.uiHost,
2722
+ artifacts: artifactSnapshot.files,
2723
+ cwd: ctx.configService.cwd,
2724
+ packageManagerAgent: ctx.configService.packageManager.agent,
2725
+ pluginRoot: ctx.configService.absolutePluginRoot,
2726
+ srcRoot: ctx.configService.absoluteSrcRoot,
2727
+ previousResult
2728
+ });
2729
+ }
2730
+ } catch (error) {
2731
+ logger_default.error(error);
2732
+ process.exitCode = 1;
2733
+ } finally {
2734
+ if (ctx) {
2735
+ for (const backend of [...targets.entries].reverse()) if (backend.descriptor.capabilities.analyze) await backend.driver?.close?.(ctx);
2736
+ }
2737
+ terminateStaleSassEmbeddedProcess();
2854
2738
  }
2855
- } catch (error) {
2856
- logger_default.error(error);
2857
- process.exitCode = 1;
2858
- } finally {
2859
- if (ctx) {
2860
- for (const backend of [...targets.entries].reverse()) if (backend.descriptor.capabilities.analyze) await backend.driver?.close?.(ctx);
2861
- }
2862
- terminateStaleSassEmbeddedProcess();
2863
- }
2739
+ });
2864
2740
  });
2865
2741
  }
2866
2742
  //#endregion
@@ -2973,9 +2849,44 @@ async function readPackageVersion(packagePath) {
2973
2849
  return manifest.version;
2974
2850
  }
2975
2851
  /** 在首次编译配置求值前准备一次元数据;预演不写文件,实际版本由 npm 同步。 */
2976
- async function prepareAutoUploadMetadata(cwd, options, action) {
2852
+ function validateAutoUploadMetadata(options, action) {
2977
2853
  const { bump, gitDesc } = options;
2978
2854
  if (action === "preview" && (bump !== void 0 || gitDesc !== void 0)) throw new Error("--bump 和 --git-desc 仅支持 upload 或 build --upload,不支持 preview。");
2855
+ if (bump !== void 0 && options.uv !== void 0) throw new Error("--bump 不能与 --uv 同时使用。");
2856
+ if (gitDesc && options.desc !== void 0) throw new Error("--git-desc 不能与 --desc 同时使用。");
2857
+ if (bump !== void 0 && ![
2858
+ "patch",
2859
+ "minor",
2860
+ "major"
2861
+ ].includes(bump)) throw new Error("--bump 仅支持 patch、minor 或 major。");
2862
+ }
2863
+ async function captureMetadataSnapshot(root) {
2864
+ const snapshots = [];
2865
+ for (const name of [
2866
+ "package.json",
2867
+ "package-lock.json",
2868
+ "npm-shrinkwrap.json"
2869
+ ]) {
2870
+ const filePath = path$1.join(root, name);
2871
+ try {
2872
+ snapshots.push({
2873
+ path: filePath,
2874
+ content: await readFile(filePath, "utf8")
2875
+ });
2876
+ } catch (error) {
2877
+ if (error.code !== "ENOENT") throw error;
2878
+ snapshots.push({ path: filePath });
2879
+ }
2880
+ }
2881
+ return snapshots;
2882
+ }
2883
+ async function restoreMetadataSnapshot(snapshots) {
2884
+ for (const snapshot of snapshots) if (snapshot.content === void 0) await rm(snapshot.path, { force: true });
2885
+ else await writeFile(snapshot.path, snapshot.content);
2886
+ }
2887
+ async function prepareAutoUploadMetadata(cwd, options, action) {
2888
+ validateAutoUploadMetadata(options, action);
2889
+ const { bump, gitDesc } = options;
2979
2890
  if (bump === void 0 && !gitDesc) return options;
2980
2891
  if (bump !== void 0 && options.uv !== void 0) throw new Error("--bump 不能与 --uv 同时使用。");
2981
2892
  if (gitDesc && options.desc !== void 0) throw new Error("--git-desc 不能与 --desc 同时使用。");
@@ -3036,6 +2947,15 @@ async function prepareAutoUploadMetadata(cwd, options, action) {
3036
2947
  if (desc !== void 0) metadata.desc = desc;
3037
2948
  return metadata;
3038
2949
  }
2950
+ /** 配置尚未确认可上传时,保留 npm version 所有可能修改文件的回滚点。 */
2951
+ async function prepareAutoUploadMetadataWithRollback(cwd, options, action) {
2952
+ validateAutoUploadMetadata(options, action);
2953
+ const snapshots = options.bump !== void 0 && !options.dryRun ? await captureMetadataSnapshot(path$1.resolve(cwd)) : [];
2954
+ return {
2955
+ options: await prepareAutoUploadMetadata(cwd, options, action),
2956
+ rollback: () => restoreMetadataSnapshot(snapshots)
2957
+ };
2958
+ }
3039
2959
  //#endregion
3040
2960
  //#region src/cli/upload/env.ts
3041
2961
  /** 凭据不经过 Vite 的 env 调试日志;文件优先级和进程环境覆盖规则保持一致。 */
@@ -3054,10 +2974,16 @@ async function loadUploadEnv(cwd, mode, root, envDir) {
3054
2974
  if (error.code === "ENOENT") return "";
3055
2975
  throw error;
3056
2976
  });
3057
- Object.assign(parsed, parseEnv(content));
2977
+ for (const [key, value] of Object.entries(parse$2(content))) {
2978
+ delete parsed[key];
2979
+ parsed[key] = value;
2980
+ }
3058
2981
  }
3059
2982
  const processEnv = { ...parsed };
3060
- for (const [key, value] of Object.entries(process.env)) if (value !== void 0) processEnv[key] = value;
2983
+ for (const [key, value] of Object.entries(process.env)) if (value !== void 0) {
2984
+ processEnv[key] = value;
2985
+ delete parsed[key];
2986
+ }
3061
2987
  expand({
3062
2988
  parsed,
3063
2989
  processEnv
@@ -3068,6 +2994,21 @@ async function loadUploadEnv(cwd, mode, root, envDir) {
3068
2994
  };
3069
2995
  }
3070
2996
  //#endregion
2997
+ //#region src/cli/upload/executionError.ts
2998
+ /** 仅描述本地执行结果,不代表远端上传已取消。 */
2999
+ var UploadExecutionError = class extends Error {
3000
+ /** timeout 为本地超时,interrupted 为本地中断,failed 为工具或传输失败。 */
3001
+ reason;
3002
+ /** not-started 表示尚未允许 SDK 执行;unknown 表示远端结果需要人工核实。 */
3003
+ remoteOutcome;
3004
+ constructor(message, reason, remoteOutcome) {
3005
+ super(message);
3006
+ this.name = "UploadExecutionError";
3007
+ this.reason = reason;
3008
+ this.remoteOutcome = remoteOutcome;
3009
+ }
3010
+ };
3011
+ //#endregion
3071
3012
  //#region src/cli/upload/result.ts
3072
3013
  /** 只接受可展示的预览结果,文件必须由本次任务生成,不能沿用旧二维码。 */
3073
3014
  async function validatePreviewResult(value, context) {
@@ -3101,67 +3042,228 @@ async function validatePreviewResult(value, context) {
3101
3042
  }
3102
3043
  //#endregion
3103
3044
  //#region src/cli/upload/process.ts
3104
- /** 隔离官方 SDK 的日志和进程级副作用,主进程通过 stdin 传入上下文。 */
3105
- async function executeUpload(platform, context, secrets, action = "upload") {
3106
- let result;
3107
- const output = await new Promise((resolve, reject) => {
3108
- const child = fork(fileURLToPath(new URL("./upload-worker.mjs", import.meta.url)), [], {
3109
- cwd: context.cwd,
3110
- env: context.env,
3111
- execArgv: [],
3112
- stdio: [
3113
- "pipe",
3114
- "pipe",
3115
- "pipe",
3116
- "ipc"
3117
- ]
3118
- });
3045
+ function readProgress(value, secrets) {
3046
+ if (!value || typeof value !== "object" || !("type" in value) || ![
3047
+ "progress",
3048
+ "log",
3049
+ "task-created",
3050
+ "version-created"
3051
+ ].includes(String(value.type))) return;
3052
+ const event = { type: value.type };
3053
+ if ("message" in value && typeof value.message === "string") event.message = redactUploadSecrets(value.message, secrets);
3054
+ if ("percent" in value && typeof value.percent === "number" && Number.isFinite(value.percent) && value.percent >= 0 && value.percent <= 100) event.percent = value.percent;
3055
+ return event;
3056
+ }
3057
+ /** 强制终止可能截断日志尾部;同时隐藏已知凭据的不完整前缀,保留普通诊断。 */
3058
+ function redactDiagnostic(message, secrets) {
3059
+ const output = message.trimEnd();
3060
+ let end = output.length;
3061
+ for (const value of secrets) {
3062
+ const secret = value.trimEnd();
3063
+ if (!secret) continue;
3064
+ let start = output.indexOf(secret[0], Math.max(0, output.length - secret.length));
3065
+ while (start !== -1) {
3066
+ let offset = 0;
3067
+ while (start + offset < output.length && output[start + offset] === secret[offset]) offset++;
3068
+ if (start + offset === output.length) {
3069
+ end = Math.min(end, start);
3070
+ break;
3071
+ }
3072
+ start = output.indexOf(secret[0], start + 1);
3073
+ }
3074
+ }
3075
+ return redactUploadSecrets(end < output.length ? `${output.slice(0, end)}[REDACTED]` : output, secrets);
3076
+ }
3077
+ /** 隔离官方 SDK;完成确认与正常退出必须同时成立,取消仅作用于本地进程。 */
3078
+ async function executeUpload(platform, context, secrets, action = "upload", options = {}) {
3079
+ const label = `${platform} ${action === "preview" ? "预览" : "上传"}`;
3080
+ if (options.signal?.aborted) throw new UploadExecutionError(`${label}已中断(SDK 尚未开始)。`, "interrupted", "not-started");
3081
+ const { result, output } = await new Promise((resolve, reject) => {
3082
+ const spawnedAt = Date.now();
3083
+ let child;
3084
+ try {
3085
+ child = fork(fileURLToPath(new URL("./upload-worker.mjs", import.meta.url)), [], {
3086
+ cwd: context.cwd,
3087
+ env: context.env,
3088
+ execArgv: [],
3089
+ stdio: [
3090
+ "pipe",
3091
+ "pipe",
3092
+ "pipe",
3093
+ "ipc"
3094
+ ],
3095
+ detached: process.platform !== "win32",
3096
+ windowsHide: true
3097
+ });
3098
+ } catch (error) {
3099
+ reject(new UploadExecutionError(redactUploadSecrets(`${label}无法启动:${String(error)}`, secrets), "failed", "not-started"));
3100
+ return;
3101
+ }
3119
3102
  let completed = false;
3120
- let exceeded = false;
3121
- let captured = "";
3122
- let cancelled = false;
3123
- const cancel = () => {
3124
- cancelled = true;
3125
- child.kill();
3103
+ let started = false;
3104
+ let settled = false;
3105
+ let result = {};
3106
+ const captured = {
3107
+ stdout: "",
3108
+ stderr: ""
3126
3109
  };
3127
- process.once("SIGINT", cancel);
3128
- process.once("SIGTERM", cancel);
3129
- const collect = (chunk) => {
3130
- if (exceeded) return;
3131
- captured += chunk;
3132
- if (captured.length > 4194304) {
3133
- exceeded = true;
3134
- child.kill();
3110
+ let capturedSize = 0;
3111
+ let failure;
3112
+ let timer;
3113
+ let closeTimer;
3114
+ const terminate = (descendants = true) => {
3115
+ if (!child.pid) return;
3116
+ const exited = child.exitCode !== null || child.signalCode !== null;
3117
+ try {
3118
+ if (descendants && !exited && process.platform !== "win32") terminateUploadDescendants(child.pid);
3119
+ } catch (error) {
3120
+ failure ??= {
3121
+ reason: "failed",
3122
+ detail: "无法清理本地子进程"
3123
+ };
3124
+ failure.detail += `;${String(error)}`;
3135
3125
  }
3126
+ try {
3127
+ terminateUploadProcess(child.pid, {
3128
+ processGroup: true,
3129
+ exited,
3130
+ lifetime: {
3131
+ start: spawnedAt,
3132
+ end: Date.now()
3133
+ }
3134
+ });
3135
+ } catch (error) {
3136
+ failure ??= {
3137
+ reason: "failed",
3138
+ detail: "无法终止本地 worker"
3139
+ };
3140
+ failure.detail += `;${String(error)}`;
3141
+ if (!exited) child.kill("SIGKILL");
3142
+ }
3143
+ };
3144
+ const stop = (reason, detail) => {
3145
+ if (settled || failure) return;
3146
+ failure = {
3147
+ reason,
3148
+ detail
3149
+ };
3150
+ clearTimeout(timer);
3151
+ terminate();
3152
+ };
3153
+ const cancel = () => stop("interrupted", "本地执行已中断");
3154
+ const onParentExit = () => terminate();
3155
+ const collect = (stream, chunk) => {
3156
+ if (failure) return;
3157
+ const remaining = 4194304 - capturedSize;
3158
+ if (chunk.length > remaining) {
3159
+ captured.stdout = "";
3160
+ captured.stderr = "";
3161
+ stop("failed", "日志超限,已省略日志以保护凭据");
3162
+ return;
3163
+ }
3164
+ captured[stream] += chunk;
3165
+ capturedSize += chunk.length;
3166
+ };
3167
+ const onStdout = (chunk) => collect("stdout", chunk);
3168
+ const onStderr = (chunk) => collect("stderr", chunk);
3169
+ const onError = (error) => stop("failed", error.message);
3170
+ const onDisconnect = () => {
3171
+ if (!completed) stop("failed", "IPC 连接中断,未收到完成确认");
3136
3172
  };
3137
- child.stdout?.setEncoding("utf8").on("data", collect);
3138
- child.stderr?.setEncoding("utf8").on("data", collect);
3139
- child.on("message", (message) => {
3140
- if (message !== null && typeof message === "object" && "type" in message && message.type === "completed" && "action" in message && message.action === action) {
3173
+ const onMessage = (message) => {
3174
+ if (settled || failure || !message || typeof message !== "object" || !("action" in message) || message.action !== action || !("type" in message)) return;
3175
+ if (message.type === "started" && !started) {
3176
+ started = true;
3177
+ child.send({
3178
+ type: "run",
3179
+ action
3180
+ }, (error) => {
3181
+ if (error) onError(error);
3182
+ });
3183
+ } else if (message.type === "progress" && "event" in message) {
3184
+ const event = readProgress(message.event, secrets);
3185
+ if (event) try {
3186
+ options.onProgress?.(event);
3187
+ } catch (error) {
3188
+ stop("failed", `进度处理失败:${String(error)}`);
3189
+ }
3190
+ } else if (message.type === "completed" && started) {
3141
3191
  completed = true;
3142
- result = "result" in message ? message.result : void 0;
3192
+ result = "result" in message && message.result && typeof message.result === "object" ? message.result : {};
3143
3193
  }
3144
- });
3145
- child.once("error", reject);
3146
- child.once("close", (code, signal) => {
3194
+ };
3195
+ const onExit = () => {
3196
+ clearTimeout(timer);
3197
+ terminate(false);
3198
+ closeTimer = setTimeout(() => {
3199
+ failure ??= {
3200
+ reason: "failed",
3201
+ detail: "worker 已退出"
3202
+ };
3203
+ failure.detail += ";日志管道未关闭,本地子进程清理未确认";
3204
+ captured.stdout = "";
3205
+ captured.stderr = "";
3206
+ child.stdin?.destroy();
3207
+ child.stdout?.destroy();
3208
+ child.stderr?.destroy();
3209
+ }, 1e3);
3210
+ };
3211
+ const cleanup = () => {
3212
+ clearTimeout(timer);
3213
+ clearTimeout(closeTimer);
3214
+ options.signal?.removeEventListener("abort", cancel);
3147
3215
  process.off("SIGINT", cancel);
3148
3216
  process.off("SIGTERM", cancel);
3149
- const safeOutput = redactUploadSecrets(captured, secrets).trim();
3150
- if (code !== 0 || !completed || exceeded || cancelled) {
3151
- reject(/* @__PURE__ */ new Error(`${platform} ${action === "preview" ? "预览" : "上传"}失败(${exceeded ? "日志超限" : signal ?? code ?? "unknown"},${completed ? "进程异常结束" : "未收到完成确认"})${safeOutput ? `\n${safeOutput}` : ""}`));
3217
+ process.off("exit", onParentExit);
3218
+ child.off("message", onMessage);
3219
+ child.off("error", onError);
3220
+ child.off("disconnect", onDisconnect);
3221
+ child.off("exit", onExit);
3222
+ child.stdout?.off("data", onStdout);
3223
+ child.stderr?.off("data", onStderr);
3224
+ child.stdin?.off("error", onError);
3225
+ };
3226
+ child.once("close", (code, signal) => {
3227
+ settled = true;
3228
+ cleanup();
3229
+ const output = [redactDiagnostic(captured.stdout, secrets), redactDiagnostic(captured.stderr, secrets)].filter(Boolean).join("\n").trim();
3230
+ if (failure || code !== 0 || !completed) {
3231
+ const detail = failure?.detail ?? `${signal ?? code ?? "unknown"},${completed ? "进程异常结束" : "未收到完成确认"}`;
3232
+ reject(new UploadExecutionError(redactUploadSecrets(`${label}失败(${detail})。${started ? "远端结果未知,请核实平台状态;未自动重试。" : "SDK 尚未开始。"}${output ? `\n${output}` : ""}`, secrets), failure?.reason ?? "failed", started ? "unknown" : "not-started"));
3152
3233
  return;
3153
3234
  }
3154
- resolve(safeOutput);
3235
+ resolve({
3236
+ result,
3237
+ output
3238
+ });
3155
3239
  });
3156
- child.stdin?.on("error", () => {});
3157
- child.stdin?.end(JSON.stringify({
3158
- platform,
3159
- context,
3160
- action
3161
- }));
3240
+ child.on("error", onError);
3241
+ child.on("disconnect", onDisconnect);
3242
+ child.on("message", onMessage);
3243
+ child.once("exit", onExit);
3244
+ child.stdout?.setEncoding("utf8").on("data", onStdout);
3245
+ child.stderr?.setEncoding("utf8").on("data", onStderr);
3246
+ child.stdin?.on("error", onError);
3247
+ process.once("exit", onParentExit);
3248
+ if (options.signal) options.signal.addEventListener("abort", cancel, { once: true });
3249
+ else {
3250
+ process.once("SIGINT", cancel);
3251
+ process.once("SIGTERM", cancel);
3252
+ }
3253
+ if (options.timeoutMs !== void 0) timer = setTimeout(stop, options.timeoutMs, "timeout", `本地执行超时(${options.timeoutMs}ms)`);
3254
+ if (options.signal?.aborted) cancel();
3255
+ else try {
3256
+ child.stdin?.end(JSON.stringify({
3257
+ platform,
3258
+ context,
3259
+ action
3260
+ }));
3261
+ } catch (error) {
3262
+ stop("failed", String(error));
3263
+ }
3162
3264
  });
3163
3265
  if (output) logger_default.info(output);
3164
- return action === "preview" ? validatePreviewResult(result, context) : void 0;
3266
+ return action === "preview" ? validatePreviewResult(result, context) : result;
3165
3267
  }
3166
3268
  //#endregion
3167
3269
  //#region src/cli/upload/project.ts
@@ -3181,6 +3283,63 @@ async function validateUploadProject(options) {
3181
3283
  await access(path$1.join(outputRoot, "app.json"));
3182
3284
  }
3183
3285
  //#endregion
3286
+ //#region src/cli/upload/report.ts
3287
+ var UploadCommandError = class extends Error {
3288
+ report;
3289
+ constructor(report) {
3290
+ super(report.error);
3291
+ this.report = report;
3292
+ this.name = "UploadCommandError";
3293
+ }
3294
+ };
3295
+ function createFailedUploadReport(error) {
3296
+ return {
3297
+ schemaVersion: 1,
3298
+ action: "upload",
3299
+ status: "failed",
3300
+ results: [],
3301
+ error: error instanceof Error ? error.message : String(error)
3302
+ };
3303
+ }
3304
+ function printUploadProgress(platform, event) {
3305
+ const percent = event.percent === void 0 ? "" : ` ${event.percent}%`;
3306
+ process.stderr.write(`[upload:${platform}] ${event.type}${percent}${event.message ? ` ${event.message}` : ""}\n`);
3307
+ }
3308
+ function printUploadSummary(report) {
3309
+ const labels = {
3310
+ "success": "上传成功",
3311
+ "failed": "失败",
3312
+ "not-run": "未执行",
3313
+ "dry-run": "仅构建",
3314
+ "unknown": "远端结果未确认"
3315
+ };
3316
+ for (const item of report.results) process.stderr.write(`[upload:${item.platform ?? "配置平台"}] ${labels[item.status]}${item.requestedVersion ? ` ${item.requestedVersion}` : ""} (${item.stage})\n`);
3317
+ }
3318
+ /** JSON 模式独占 stdout,构建配置及第三方工具的普通日志只进入 stderr。 */
3319
+ async function outputUploadReport(json, run) {
3320
+ const stdoutWrite = process.stdout.write;
3321
+ if (json) process.stdout.write = process.stderr.write.bind(process.stderr);
3322
+ try {
3323
+ let report;
3324
+ try {
3325
+ report = await run();
3326
+ } catch (error) {
3327
+ report = error instanceof UploadCommandError ? error.report : createFailedUploadReport(error);
3328
+ }
3329
+ if (json) {
3330
+ await new Promise((resolve, reject) => {
3331
+ stdoutWrite.call(process.stdout, `${JSON.stringify(report)}\n`, "utf8", (error) => error ? reject(error) : resolve());
3332
+ });
3333
+ if (report.status === "failed") process.exitCode = 1;
3334
+ } else {
3335
+ printUploadSummary(report);
3336
+ if (report.status === "failed") throw new UploadCommandError(report);
3337
+ }
3338
+ } finally {
3339
+ process.stdout.write = stdoutWrite;
3340
+ }
3341
+ }
3342
+ //#endregion
3184
3343
  //#region src/cli/upload/builtProject.ts
3185
3344
  /** 仅由显式上传或预览入口读取参数,不执行构建或调用平台工具。 */
3186
3345
  async function createUploadTarget(config, options, action) {
@@ -3224,7 +3383,7 @@ async function createUploadTarget(config, options, action) {
3224
3383
  return target;
3225
3384
  }
3226
3385
  /** 只消费本次构建的产物;产物校验通过前不会准备凭据或调用平台工具。 */
3227
- async function executeUploadTarget(target, options, action) {
3386
+ async function executeUploadTarget(target, options, action, execution = {}) {
3228
3387
  const { platform, context } = target;
3229
3388
  if (!target.outDir) throw new Error("本次构建未写出 app.json,已阻止上传或预览旧产物。");
3230
3389
  await validateUploadProject({
@@ -3238,7 +3397,11 @@ async function executeUploadTarget(target, options, action) {
3238
3397
  return;
3239
3398
  }
3240
3399
  const upload = await prepareUpload(platform, context, action);
3241
- const result = await executeUpload(platform, context, upload.secrets, action);
3400
+ execution.onUpload?.();
3401
+ const result = await executeUpload(platform, context, upload.secrets, action, {
3402
+ ...execution,
3403
+ onProgress: execution.onProgress ?? ((event) => printUploadProgress(platform, event))
3404
+ });
3242
3405
  if (action === "preview") {
3243
3406
  if (!result) throw new Error("预览工具未返回二维码或预览链接。");
3244
3407
  for (const [label, value] of [
@@ -3248,6 +3411,7 @@ async function executeUploadTarget(target, options, action) {
3248
3411
  ]) if (value) logger_default.info(`[preview:${platform}] ${label}:${redactUploadSecrets(value, upload.secrets)}`);
3249
3412
  logger_default.success(`[preview:${platform}] 预览已生成(未上传开发版本、未提审、未正式发布)。`);
3250
3413
  } else logger_default.success(`[upload:${platform}] ${context.version} 上传完成(未提审、未正式发布)。`);
3414
+ return result;
3251
3415
  }
3252
3416
  //#endregion
3253
3417
  //#region src/cli/upload/options.ts
@@ -3276,6 +3440,14 @@ function readUploadMetadata(cli) {
3276
3440
  bump: typeof values.bump === "string" ? values.bump : void 0
3277
3441
  };
3278
3442
  }
3443
+ /** 超时只约束单个平台的 SDK 进程,不含构建;未指定时不改变原有等待行为。 */
3444
+ function resolveUploadTimeout(value) {
3445
+ if (value === void 0) return;
3446
+ const seconds = typeof value === "number" ? value : Number(value);
3447
+ const milliseconds = Math.round(seconds * 1e3);
3448
+ if (typeof value !== "string" && typeof value !== "number" || !Number.isFinite(milliseconds) || milliseconds / 1e3 !== seconds || milliseconds <= 0 || milliseconds > 2147483647) throw new Error("--timeout 必须为正数秒,精度不超过毫秒且不能超过 2147483.647 秒。");
3449
+ return milliseconds;
3450
+ }
3279
3451
  /** 普通构建不读取上传元数据;上传必须是显式的一次性构建操作。 */
3280
3452
  function resolveBuildUploadOptions(cli, options) {
3281
3453
  if (options.upload !== true) {
@@ -3324,26 +3496,36 @@ function scheduleCompletedProductionBuildExit(options, analyzeHandle) {
3324
3496
  }, 0).unref?.();
3325
3497
  }
3326
3498
  function registerBuildCommand(cli) {
3327
- cli.command("build [root]", "build for production").option("--target <target>", `[string] transpile target (default: 'modules')`).option("--outDir <dir>", `[string] output directory (default: dist)`).option("-p, --platform <platform>", `[string] target platform (weapp | web | all)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).option("--sourcemap [output]", `[boolean | "inline" | "hidden"] output source maps for build (default: false)`).option("--minify [minifier]", "[boolean | \"terser\" | \"esbuild\"] enable/disable minification, or specify minifier to use (default: esbuild)").option("--emptyOutDir", `[boolean] force empty outDir when it's outside of root`).option("-w, --watch", `[boolean] rebuilds when modules have changed on disk`).option("--skipNpm", `[boolean] if skip npm build`).option("-o, --open", `[boolean] open ide`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--no-open-recovery", "[boolean] disable automatic Wechat DevTools close-and-reopen recovery").option("--ui", `[boolean] 启动调试 UI(当前提供分析视图)`, { default: false }).option("--analyze", `[boolean] 输出分包分析仪表盘`, { default: false }).option("--scope <scope>", `[string] 局部构建范围,例如 main,packages/order`).option("--upload", "[boolean] upload the mini program after this build succeeds").option("--uv <version>", "[string] upload version override (requires --upload)").option("--desc <text>", "[string] upload description override (requires --upload)").option("--bump <release>", "[string] increment local package version (patch | minor | major; requires --upload)").option("--git-desc", "[boolean] use the latest Git commit subject (requires --upload)").option("--dry-run", "[boolean] validate upload output without credentials or SDK calls (requires --upload)").action(async (root, options) => {
3499
+ cli.command("build [root]", "build for production").option("--target <target>", `[string] transpile target (default: 'modules')`).option("--outDir <dir>", `[string] output directory (default: dist)`).option("-p, --platform <platform>", `[string] target platform (weapp | web | all)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).option("--sourcemap [output]", `[boolean | "inline" | "hidden"] output source maps for build (default: false)`).option("--minify [minifier]", "[boolean | \"terser\" | \"esbuild\"] enable/disable minification, or specify minifier to use (default: esbuild)").option("--emptyOutDir", `[boolean] force empty outDir when it's outside of root`).option("-w, --watch", `[boolean] rebuilds when modules have changed on disk`).option("--skipNpm", `[boolean] if skip npm build`).option("-o, --open", `[boolean] open ide`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--no-open-recovery", "[boolean] disable automatic target-project open retry (preserves existing IDE windows)").option("--ui", `[boolean] 启动调试 UI(当前提供分析视图)`, { default: false }).option("--ui-host <host>", `[string] 启动调试 UI 并选择宿主(standalone | hub)`).option("--analyze", `[boolean] 输出分包分析仪表盘`, { default: false }).option("--scope <scope>", `[string] 局部构建范围,例如 main,packages/order`).option("--upload", "[boolean] upload the mini program after this build succeeds").option("--uv <version>", "[string] upload version override (requires --upload)").option("--desc <text>", "[string] upload description override (requires --upload)").option("--bump <release>", "[string] increment local package version (patch | minor | major; requires --upload)").option("--git-desc", "[boolean] use the latest Git commit subject (requires --upload)").option("--dry-run", "[boolean] validate upload output without credentials or SDK calls (requires --upload)").action(async (root, options) => {
3328
3500
  let analyzeHandle;
3501
+ let session;
3329
3502
  let ctx;
3330
3503
  let targets;
3504
+ let uploadMetadataRollback;
3331
3505
  let buildCompleted = false;
3506
+ let buildFailed = false;
3332
3507
  try {
3333
3508
  options = { ...options };
3334
3509
  filterDuplicateOptions(options);
3510
+ options.uiHost = parseDashboardUiHost(options.uiHost);
3335
3511
  let uploadOptions = resolveBuildUploadOptions(cli, options);
3336
3512
  setCommandNodeEnv("production");
3337
3513
  const cwd = root ?? process.cwd();
3338
3514
  const configFile = resolveConfigFile(options);
3339
3515
  targets = resolveRuntimeTargets(options);
3516
+ if (uploadOptions) validateAutoUploadMetadata(uploadOptions, "upload");
3340
3517
  if (uploadOptions && !getBackendForCapability(targets, "miniprogram", "build")) throw new Error("--upload 仅支持包含小程序目标的构建,不能用于纯 Web 构建。");
3341
- if (uploadOptions) uploadOptions = await prepareAutoUploadMetadata(cwd, uploadOptions, "upload");
3342
3518
  const inlineConfig = createInlineConfig(targets, {
3343
3519
  scope: options.scope,
3344
3520
  inlineConfig: createBuildInlineConfig(options)
3345
3521
  });
3346
- ctx = await createCompilerContext({
3522
+ if (uploadOptions) {
3523
+ const prepared = await prepareAutoUploadMetadataWithRollback(cwd, uploadOptions, "upload");
3524
+ uploadOptions = prepared.options;
3525
+ uploadMetadataRollback = prepared.rollback;
3526
+ }
3527
+ session = new CompilerSession();
3528
+ ctx = await session.initialize({
3347
3529
  cwd,
3348
3530
  mode: options.mode ?? "production",
3349
3531
  configFile,
@@ -3353,6 +3535,14 @@ function registerBuildCommand(cli) {
3353
3535
  emitDefaultAutoImportOutputs: false,
3354
3536
  preloadAppEntry: false
3355
3537
  });
3538
+ targets = resolveConfiguredRuntimeTargets(targets, ctx.configService.options?.sourceConfig?.weapp?.platform);
3539
+ if (uploadOptions && !getBackendForCapability(targets, "miniprogram", "build")) {
3540
+ await uploadMetadataRollback?.();
3541
+ uploadMetadataRollback = void 0;
3542
+ throw new Error("--upload 仅支持包含小程序目标的构建,不能用于纯 Web 构建。");
3543
+ }
3544
+ uploadMetadataRollback = void 0;
3545
+ for (const backend of targets.select("build")) session.onClose(() => backend.driver.close(ctx));
3356
3546
  const { configService } = ctx;
3357
3547
  const uploadTarget = uploadOptions ? await createUploadTarget(configService, uploadOptions, "upload") : void 0;
3358
3548
  const miniBackend = getBackendForCapability(targets, "miniprogram", "build");
@@ -3362,7 +3552,7 @@ function registerBuildCommand(cli) {
3362
3552
  for (const backend of targets.select("build")) {
3363
3553
  if (backend.descriptor.id === "miniprogram") {
3364
3554
  const miniBuildStartedAt = Date.now();
3365
- const output = await backend.driver.build(ctx, options);
3555
+ const output = await session.run(() => backend.driver.build(ctx, options));
3366
3556
  const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
3367
3557
  logger_default.success(`小程序构建完成,耗时:${colors.green(formatDuration(miniBuildDurationMs))}`);
3368
3558
  if (!Array.isArray(output) && "output" in output) logBuildPackageSizeReport({
@@ -3379,6 +3569,7 @@ function registerBuildCommand(cli) {
3379
3569
  const analyzeDurationMs = Date.now() - analyzeStartedAt;
3380
3570
  analyzeHandle = await startAnalyzeDashboard(analyzeResult, {
3381
3571
  watch: true,
3572
+ uiHost: options.uiHost,
3382
3573
  artifacts: artifactSnapshot.files,
3383
3574
  cwd: configService.cwd,
3384
3575
  packageManagerAgent: configService.packageManager.agent,
@@ -3408,7 +3599,7 @@ function registerBuildCommand(cli) {
3408
3599
  if (backend.descriptor.id !== "web" || !webConfig?.enabled) continue;
3409
3600
  const webBuildStartedAt = Date.now();
3410
3601
  try {
3411
- await backend.driver.build(ctx, options);
3602
+ await session.run(() => backend.driver.build(ctx, options));
3412
3603
  const webBuildDurationMs = Date.now() - webBuildStartedAt;
3413
3604
  logger_default.success(`Web 构建完成,输出目录:${colors.green(configService.relativeCwd(webConfig.outDir))},耗时:${colors.green(formatDuration(webBuildDurationMs))}`);
3414
3605
  emitDashboardEvents$1(analyzeHandle, [{
@@ -3451,9 +3642,12 @@ function registerBuildCommand(cli) {
3451
3642
  }
3452
3643
  if (analyzeHandle) await analyzeHandle.waitForExit();
3453
3644
  buildCompleted = true;
3645
+ } catch (error) {
3646
+ buildFailed = true;
3647
+ throw error;
3454
3648
  } finally {
3455
- if (ctx && targets) for (const backend of [...targets.select("build")].reverse()) await backend.driver.close(ctx);
3456
- terminateStaleSassEmbeddedProcess();
3649
+ if (buildFailed) await session?.close().catch((error) => logger_default.error(error));
3650
+ else await session?.close();
3457
3651
  if (buildCompleted) scheduleCompletedProductionBuildExit(options, analyzeHandle);
3458
3652
  }
3459
3653
  });
@@ -3466,6 +3660,39 @@ function registerCloseCommand(cli) {
3466
3660
  });
3467
3661
  }
3468
3662
  //#endregion
3663
+ //#region src/cli/commands/doctor.ts
3664
+ function registerDoctorCommand(cli) {
3665
+ cli.command("doctor [root]", "检查项目、源码、产物与显式选择的宿主证据").option("--targets <targets>", "编译目标,逗号分隔(默认 weapp)").option("--platform <platform>", "单个编译目标").option("--source <directory>", "静态源码目录(默认 src,不执行配置推断)").option("--artifact <directory>", "复用最终产物目录(新鲜度未验证)").option("--build", "显式执行配置和构建,使用独立输出目录").option("--runtime", "连接已打开项目的宿主探针,不启动 IDE").option("--runtime-port <port>", "已有 automator 会话的端口(默认项目派生端口)").option("--runtime-cli <path>", "只读检查所选 CLI 的可执行条件,不回退安装").option("--runtime-service-port <port>", "检查指定回环 TCP 服务监听,不推断宿主身份").option("--runtime-login", "显式运行原生登录查询,可能启动 IDE;需要 --runtime-cli").option("--format <format>", "terminal | json | sarif", { default: "terminal" }).action(async (root, options) => {
3666
+ const { formatDoctorReport, runDoctor } = await import("./doctor.mjs");
3667
+ const format = options.format;
3668
+ if (![
3669
+ "terminal",
3670
+ "json",
3671
+ "sarif"
3672
+ ].includes(format)) {
3673
+ process.stderr.write("doctor: format 必须为 terminal、json 或 sarif\n");
3674
+ process.exitCode = 2;
3675
+ return;
3676
+ }
3677
+ const report = await runDoctor({
3678
+ cwd: root,
3679
+ targets: String(options.targets ?? options.platform ?? "weapp").split(",").map((item) => item.trim()),
3680
+ source: options.source,
3681
+ artifact: options.artifact,
3682
+ build: options.build === true,
3683
+ runtime: options.runtime === true,
3684
+ configFile: options.config,
3685
+ runtimePort: options.runtimePort === void 0 ? void 0 : Number(options.runtimePort),
3686
+ runtimeCliPath: options.runtimeCli,
3687
+ runtimeServicePort: options.runtimeServicePort === void 0 ? void 0 : Number(options.runtimeServicePort),
3688
+ runtimeLogin: options.runtimeLogin === true ? true : void 0,
3689
+ onBuildLog: (text) => process.stderr.write(text)
3690
+ });
3691
+ process.stdout.write(`${formatDoctorReport(report, format)}\n`);
3692
+ process.exitCode = report.exitCode;
3693
+ });
3694
+ }
3695
+ //#endregion
3469
3696
  //#region src/schematics.ts
3470
3697
  function composePath(outDir, filename) {
3471
3698
  return `${outDir}${outDir ? "/" : ""}${filename}`;
@@ -3550,60 +3777,6 @@ async function generate(options) {
3550
3777
  }
3551
3778
  }
3552
3779
  //#endregion
3553
- //#region src/cli/loadConfig.ts
3554
- async function loadConfig(configFile) {
3555
- const cwd = process.cwd();
3556
- let resolvedConfigFile = configFile;
3557
- if (resolvedConfigFile && !path.isAbsolute(resolvedConfigFile)) resolvedConfigFile = path.resolve(cwd, resolvedConfigFile);
3558
- const configEnv = {
3559
- command: "serve",
3560
- mode: "development"
3561
- };
3562
- let loaded;
3563
- try {
3564
- loaded = await loadViteConfigFile(configEnv, resolvedConfigFile, cwd, void 0, void 0, "runner");
3565
- } catch (error) {
3566
- const cjsError = createCjsConfigLoadError({
3567
- error,
3568
- configPath: resolvedConfigFile,
3569
- cwd
3570
- });
3571
- if (cjsError) throw cjsError;
3572
- throw error;
3573
- }
3574
- const weappConfigFilePath = await resolveWeappConfigFile({
3575
- root: cwd,
3576
- specified: resolvedConfigFile
3577
- });
3578
- let weappLoaded;
3579
- if (weappConfigFilePath) {
3580
- const normalizedWeappPath = path.resolve(weappConfigFilePath);
3581
- const normalizedLoadedPath = loaded?.path ? path.resolve(loaded.path) : void 0;
3582
- if (normalizedLoadedPath && normalizedLoadedPath === normalizedWeappPath) weappLoaded = loaded;
3583
- else try {
3584
- weappLoaded = await loadViteConfigFile(configEnv, weappConfigFilePath, cwd, void 0, void 0, "runner");
3585
- } catch (error) {
3586
- const cjsError = createCjsConfigLoadError({
3587
- error,
3588
- configPath: weappConfigFilePath,
3589
- cwd
3590
- });
3591
- if (cjsError) throw cjsError;
3592
- throw error;
3593
- }
3594
- }
3595
- if (!loaded && !weappLoaded) return;
3596
- const config = weappLoaded?.config ? defu(weappLoaded.config, loaded?.config ?? {}) : loaded?.config ?? {};
3597
- const dependencySet = /* @__PURE__ */ new Set();
3598
- for (const dependency of loaded?.dependencies ?? []) dependencySet.add(dependency);
3599
- for (const dependency of weappLoaded?.dependencies ?? []) dependencySet.add(dependency);
3600
- return {
3601
- config,
3602
- path: weappLoaded?.path ?? loaded?.path ?? resolvedConfigFile,
3603
- dependencies: Array.from(dependencySet)
3604
- };
3605
- }
3606
- //#endregion
3607
3780
  //#region src/cli/commands/generate.ts
3608
3781
  function registerGenerateCommand(cli) {
3609
3782
  cli.command("g [filepath]", "generate component").alias("generate").option("-a, --app", "type app").option("-p, --page", "type app").option("-n, --name <name>", "filename").action(async (filepath, options) => {
@@ -3643,6 +3816,7 @@ const DEFAULT_FORWARD_CONSOLE_LEVELS = [
3643
3816
  let activeForwardConsoleSession;
3644
3817
  let activeForwardConsoleBridgeOptions;
3645
3818
  let activeForwardConsoleStart;
3819
+ let activeForwardConsoleClose;
3646
3820
  let forwardConsoleLifecycle = 0;
3647
3821
  const FORWARD_CONSOLE_RETRY_DELAY_MS = 1e3;
3648
3822
  const FORWARD_CONSOLE_RETRY_TIMES = 5;
@@ -3788,15 +3962,9 @@ async function pauseActiveForwardConsole() {
3788
3962
  if (activeForwardConsoleStart) return await activeForwardConsoleStart;
3789
3963
  if (activeForwardConsoleSession) return true;
3790
3964
  const startTask = (async () => {
3965
+ let resumedSession;
3791
3966
  try {
3792
- const resumedSession = await startForwardConsoleBridge(bridgeOptions);
3793
- if (lifecycle !== forwardConsoleLifecycle) {
3794
- await resumedSession.close();
3795
- return false;
3796
- }
3797
- activeForwardConsoleSession = resumedSession;
3798
- activeForwardConsoleBridgeOptions = bridgeOptions;
3799
- return true;
3967
+ resumedSession = await startForwardConsoleBridge(bridgeOptions);
3800
3968
  } catch (error) {
3801
3969
  if (lifecycle === forwardConsoleLifecycle) {
3802
3970
  const message = error instanceof Error ? error.message : String(error);
@@ -3804,6 +3972,13 @@ async function pauseActiveForwardConsole() {
3804
3972
  }
3805
3973
  return false;
3806
3974
  }
3975
+ if (lifecycle !== forwardConsoleLifecycle) {
3976
+ await resumedSession.close();
3977
+ return false;
3978
+ }
3979
+ activeForwardConsoleSession = resumedSession;
3980
+ activeForwardConsoleBridgeOptions = bridgeOptions;
3981
+ return true;
3807
3982
  })();
3808
3983
  activeForwardConsoleStart = startTask;
3809
3984
  try {
@@ -3816,20 +3991,29 @@ async function pauseActiveForwardConsole() {
3816
3991
  /**
3817
3992
  * @description 关闭当前 DevTools 日志桥并释放共享 automator 会话。
3818
3993
  */
3819
- async function closeActiveForwardConsole() {
3994
+ function closeActiveForwardConsole() {
3995
+ if (activeForwardConsoleClose) return activeForwardConsoleClose;
3820
3996
  forwardConsoleLifecycle += 1;
3821
3997
  const session = activeForwardConsoleSession;
3822
3998
  const pendingStart = activeForwardConsoleStart;
3823
3999
  activeForwardConsoleSession = void 0;
3824
4000
  activeForwardConsoleBridgeOptions = void 0;
3825
- await session?.close();
3826
- await pendingStart?.catch(() => {});
4001
+ const closing = (async () => {
4002
+ const errors = (await Promise.allSettled([Promise.resolve().then(() => session?.close()), pendingStart])).filter((result) => result.status === "rejected").map((result) => result.reason);
4003
+ if (errors.length === 1) throw errors[0];
4004
+ if (errors.length > 1) throw new AggregateError(errors, "DevTools 日志桥关闭失败");
4005
+ })();
4006
+ activeForwardConsoleClose = closing;
4007
+ closing.finally(() => {
4008
+ if (activeForwardConsoleClose === closing) activeForwardConsoleClose = void 0;
4009
+ }).catch(() => {});
4010
+ return closing;
3827
4011
  }
3828
4012
  /**
3829
4013
  * @description 在 weapp 开发态按需启动控制台转发。
3830
4014
  */
3831
4015
  async function maybeStartForwardConsole(options) {
3832
- if (options.platform !== "weapp") return false;
4016
+ if (options.platform !== "weapp" || activeForwardConsoleClose) return false;
3833
4017
  const multiPlatform = options.weappViteConfig?.multiPlatform;
3834
4018
  const generatedProjectConfig = typeof multiPlatform === "object" && multiPlatform.projectConfigs !== void 0;
3835
4019
  const projectPath = resolveIdeProjectRoot(options.mpDistRoot, options.cwd, generatedProjectConfig);
@@ -3871,7 +4055,7 @@ async function maybeStartForwardConsole(options) {
3871
4055
  openedOnly: true,
3872
4056
  preferOpenedSession: true
3873
4057
  };
3874
- return await activateSession(await startForwardConsoleBridge(recoveredOptions), recoveredOptions);
4058
+ return activateSession(await startForwardConsoleBridge(recoveredOptions), recoveredOptions);
3875
4059
  } catch (recoveryError) {
3876
4060
  if (lifecycle !== forwardConsoleLifecycle) return false;
3877
4061
  const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
@@ -3880,7 +4064,7 @@ async function maybeStartForwardConsole(options) {
3880
4064
  }
3881
4065
  };
3882
4066
  try {
3883
- return await activateSession(await startForwardConsoleBridge(bridgeOptions), bridgeOptions);
4067
+ return activateSession(await startForwardConsoleBridge(bridgeOptions), bridgeOptions);
3884
4068
  } catch (error) {
3885
4069
  if (lifecycle !== forwardConsoleLifecycle) return false;
3886
4070
  if (options.recoverAutomatorSession && isRecoverableAutomatorStartError(error)) return await recoverAutomatorSession();
@@ -3896,7 +4080,7 @@ async function maybeStartForwardConsole(options) {
3896
4080
  preferOpenedSession: true
3897
4081
  };
3898
4082
  try {
3899
- return await activateSession(await startForwardConsoleBridge(fallbackOptions), fallbackOptions);
4083
+ return activateSession(await startForwardConsoleBridge(fallbackOptions), fallbackOptions);
3900
4084
  } catch (fallbackError) {
3901
4085
  if (lifecycle !== forwardConsoleLifecycle) return false;
3902
4086
  const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
@@ -3917,19 +4101,6 @@ async function maybeStartForwardConsole(options) {
3917
4101
  function isFiniteNumber(value) {
3918
4102
  return typeof value === "number" && Number.isFinite(value);
3919
4103
  }
3920
- function parseLatestHmrProfileSample(content) {
3921
- const lines = content.split(/\r?\n/);
3922
- for (let index = lines.length - 1; index >= 0; index -= 1) {
3923
- const trimmed = lines[index]?.trim();
3924
- if (!trimmed) continue;
3925
- try {
3926
- const parsed = JSON.parse(trimmed);
3927
- if (isFiniteNumber(parsed.totalMs)) return parsed;
3928
- } catch {
3929
- continue;
3930
- }
3931
- }
3932
- }
3933
4104
  function formatPhaseHint(sample) {
3934
4105
  const topPhase = [
3935
4106
  {
@@ -4119,7 +4290,8 @@ async function readLatestHmrProfileSummary(options) {
4119
4290
  if (!profilePath) return;
4120
4291
  const content = await fs$1.readFile(profilePath, "utf8").catch(() => void 0);
4121
4292
  if (!content) return;
4122
- const sample = parseLatestHmrProfileSample(content);
4293
+ const { samples, skippedLineCount } = readHmrProfileLines(content);
4294
+ const sample = samples.at(-1);
4123
4295
  if (!sample || !isFiniteNumber(sample.totalMs)) return;
4124
4296
  const relativeCwd = options.relativeCwd ?? ((value) => value);
4125
4297
  const segments = [`[hmr] 最近一次热更新 ${sample.totalMs.toFixed(2)} ms`];
@@ -4127,7 +4299,8 @@ async function readLatestHmrProfileSummary(options) {
4127
4299
  if (sample.file) segments.push(relativeCwd(sample.file));
4128
4300
  const phaseHint = formatPhaseHint(sample);
4129
4301
  if (phaseHint) segments.push(`主耗时 ${phaseHint}`);
4130
- if (isFiniteNumber(sample.loadCount) || isFiniteNumber(sample.resolveCount) || isFiniteNumber(sample.chunkEmitCount) || isFiniteNumber(sample.skippedLoadedCount)) segments.push(`load/resolve/chunk/skip ${sample.loadCount ?? 0}/${sample.resolveCount ?? 0}/${sample.chunkEmitCount ?? 0}/${sample.skippedLoadedCount ?? 0}`);
4302
+ if (isFiniteNumber(sample.loadCount) || isFiniteNumber(sample.resolveCount) || isFiniteNumber(sample.chunkEmitCount) || isFiniteNumber(sample.skippedLoadedCount)) segments.push(`load/resolve/chunk/skip ${sample.loadCount ?? "unknown"}/${sample.resolveCount ?? "unknown"}/${sample.chunkEmitCount ?? "unknown"}/${sample.skippedLoadedCount ?? "unknown"}`);
4303
+ if (skippedLineCount) segments.push(`忽略 ${skippedLineCount} 条不兼容、未完成或无效记录`);
4131
4304
  return {
4132
4305
  file: sample.file,
4133
4306
  profilePath,
@@ -4180,20 +4353,19 @@ async function runIdeDoctor(projectPath, options) {
4180
4353
  status: "unknown"
4181
4354
  };
4182
4355
  if (resolvedCli.cliPath) try {
4183
- await isWechatIdeLoggedIn({
4184
- nonInteractive: true,
4185
- silent: true
4186
- });
4187
- loginCheck = {
4188
- status: "ok",
4189
- value: true
4356
+ const result = await queryWechatIdeLogin(resolvedCli.cliPath, { timeout: 3e3 });
4357
+ loginCheck = result.status === "success" ? {
4358
+ status: result.login ? "ok" : "warning",
4359
+ value: result.login,
4360
+ ...result.login ? {} : { message: "原生 CLI 明确返回未登录,请打开微信开发者工具完成登录。" }
4361
+ } : {
4362
+ status: "unknown",
4363
+ message: `登录查询未确认状态(${result.reason}),不能据此认定未登录。`
4190
4364
  };
4191
- } catch (error) {
4365
+ } catch {
4192
4366
  loginCheck = {
4193
- fix: "打开微信开发者工具完成登录后重试,或执行 `wv ide doctor --json` 查看 CLI 错误。",
4194
- message: error instanceof Error ? error.message : String(error),
4195
- status: "warning",
4196
- value: false
4367
+ message: "登录查询执行失败,登录状态仍未确认。",
4368
+ status: "unknown"
4197
4369
  };
4198
4370
  }
4199
4371
  const projectCheck = projectPath ? {
@@ -4222,11 +4394,20 @@ async function runIdeDoctor(projectPath, options) {
4222
4394
  status: "ok",
4223
4395
  value: true
4224
4396
  };
4225
- if (typeof miniProgram.toolInfo === "function") toolCheck = {
4226
- status: "ok",
4227
- value: await miniProgram.toolInfo()
4228
- };
4229
- miniProgram.disconnect?.();
4397
+ try {
4398
+ if (typeof miniProgram.toolInfo === "function") toolCheck = {
4399
+ status: "ok",
4400
+ value: await miniProgram.toolInfo()
4401
+ };
4402
+ } catch {
4403
+ toolCheck = {
4404
+ status: "warning",
4405
+ message: "项目自动化连接已建立,但 Tool.getInfo 未能返回工具信息;宿主版本仍未确认。",
4406
+ fix: "确认目标项目模拟器就绪后重新运行 doctor;此结果不证明登录失效。"
4407
+ };
4408
+ } finally {
4409
+ miniProgram.disconnect?.();
4410
+ }
4230
4411
  } catch (error) {
4231
4412
  automatorCheck = {
4232
4413
  fix: "先执行 `wv open` 打开项目;若服务端口已开启,关闭多余 DevTools 窗口后重试。",
@@ -4375,7 +4556,7 @@ async function runIdeCommand(action, root, options) {
4375
4556
  * @description 注册 IDE 相关子命令。
4376
4557
  */
4377
4558
  function registerIdeCommand(cli) {
4378
- cli.command("ide [action] [root]", "run Wechat DevTools utility actions and log bridge commands").option("-o, --open", "[boolean] open ide before attaching log bridge").option("-p, --platform <platform>", "[string] target platform (weapp | web)").option("--project-config <path>", "[string] project config path (miniprogram only)").option("--json", "[boolean] output the doctor report as JSON").option("--ticket <value>", "[string] ticket used by `ide ticket:set`").option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--strict", "[boolean] fail when doctor finds blocking environment errors").option("--no-open-recovery", "[boolean] disable automatic Wechat DevTools close-and-reopen recovery").action(async (action, root, options) => {
4559
+ cli.command("ide [action] [root]", "run Wechat DevTools utility actions and log bridge commands").option("-o, --open", "[boolean] open ide before attaching log bridge").option("-p, --platform <platform>", "[string] target platform (weapp | web)").option("--project-config <path>", "[string] project config path (miniprogram only)").option("--json", "[boolean] output the doctor report as JSON").option("--ticket <value>", "[string] ticket used by `ide ticket:set`").option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--strict", "[boolean] fail when doctor finds blocking environment errors").option("--no-open-recovery", "[boolean] disable automatic target-project open retry (preserves existing IDE windows)").action(async (action, root, options) => {
4379
4560
  await runIdeCommand(action, root, options);
4380
4561
  });
4381
4562
  }
@@ -4391,405 +4572,6 @@ function registerInitCommand(cli) {
4391
4572
  });
4392
4573
  }
4393
4574
  //#endregion
4394
- //#region src/cli/mcpClient/shared.ts
4395
- const CODEX_BLOCK_PREFIX = "# >>> weapp-vite mcp ";
4396
- const CODEX_BLOCK_SUFFIX = " >>>";
4397
- const CODEX_BLOCK_END_PREFIX = "# <<< weapp-vite mcp ";
4398
- const CODEX_BLOCK_END_SUFFIX = " <<<";
4399
- const DEFAULT_HTTP_TIMEOUT_MS = 1500;
4400
- const WORKSPACE_FOLDER_TOKEN = "${workspaceFolder}";
4401
- const REG_CodexUrl = /^\s*url\s*=\s*"([^"]+)"/m;
4402
- const REG_CodexCommand = /^\s*command\s*=\s*"([^"]+)"/m;
4403
- const REG_CodexArgs = /^\s*args\s*=\s*\[([^\]]*)\]/m;
4404
- const REG_FirstTomlString = /"([^"]+)"/;
4405
- function resolveTargetWorkspaceRoot(target) {
4406
- if (target.client === "cursor") return path.dirname(path.dirname(target.configPath));
4407
- if (target.client === "claude-code") return path.dirname(target.configPath);
4408
- return process.cwd();
4409
- }
4410
- function escapeRegExp(value) {
4411
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4412
- }
4413
- function renderTomlString(value) {
4414
- return JSON.stringify(value);
4415
- }
4416
- function sanitizeServerNameSegment(value) {
4417
- return value.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
4418
- }
4419
- function resolveServerName(workspaceRoot) {
4420
- return `weapp-vite-${sanitizeServerNameSegment(path.basename(workspaceRoot)) || "project"}`;
4421
- }
4422
- function resolveNodeBinCommand() {
4423
- return process.execPath || "node";
4424
- }
4425
- function resolveProjectCliBin(workspaceRoot) {
4426
- return path.join(workspaceRoot, "node_modules", "weapp-vite", "bin", "weapp-vite.js");
4427
- }
4428
- function renderCommandArgs(args) {
4429
- return args.map(renderTomlString).join(", ");
4430
- }
4431
- function renderCodexBlock(serverName, entry) {
4432
- const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
4433
- const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
4434
- if (entry.url) return `${startMarker}
4435
- [mcp_servers.${serverName}]
4436
- url = ${renderTomlString(entry.url)}
4437
- ${endMarker}
4438
- `;
4439
- return `${startMarker}
4440
- [mcp_servers.${serverName}]
4441
- command = ${renderTomlString(entry.command ?? resolveNodeBinCommand())}
4442
- args = [${renderCommandArgs(entry.args ?? [])}]
4443
- ${endMarker}
4444
- `;
4445
- }
4446
- function renderJsonPreview(serverName, entry) {
4447
- return `${JSON.stringify({ mcpServers: { [serverName]: entry } }, null, 2)}
4448
- `;
4449
- }
4450
- function resolveCommandEntry(client, workspaceRoot) {
4451
- const binPath = resolveProjectCliBin(workspaceRoot);
4452
- if (client === "cursor") return {
4453
- command: "node",
4454
- args: [
4455
- `${WORKSPACE_FOLDER_TOKEN}/node_modules/weapp-vite/bin/weapp-vite.js`,
4456
- "mcp",
4457
- "--workspace-root",
4458
- WORKSPACE_FOLDER_TOKEN
4459
- ]
4460
- };
4461
- return {
4462
- command: resolveNodeBinCommand(),
4463
- args: [
4464
- binPath,
4465
- "mcp",
4466
- "--workspace-root",
4467
- workspaceRoot
4468
- ]
4469
- };
4470
- }
4471
- function resolveHttpEntry(client, url) {
4472
- if (client === "claude-code") return {
4473
- type: "http",
4474
- url
4475
- };
4476
- return { url };
4477
- }
4478
- function resolveConfigEntry(client, transport, workspaceRoot, url) {
4479
- return transport === "http" ? resolveHttpEntry(client, url ?? "") : resolveCommandEntry(client, workspaceRoot);
4480
- }
4481
- function resolveCodexBlockPattern(serverName) {
4482
- const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
4483
- const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
4484
- return new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
4485
- }
4486
- function upsertCodexManagedBlock(content, serverName, block) {
4487
- const withoutExisting = content.trimEnd().replace(resolveCodexBlockPattern(serverName), "").trimEnd();
4488
- if (!withoutExisting) return block;
4489
- return `${withoutExisting}\n\n${block}`;
4490
- }
4491
- function parseJsonConfig(content, configPath) {
4492
- if (!content.trim()) return {};
4493
- try {
4494
- return JSON.parse(content);
4495
- } catch (error) {
4496
- const message = error instanceof Error ? error.message : String(error);
4497
- throw new Error(`无法解析 MCP 配置文件 ${configPath}:${message}`);
4498
- }
4499
- }
4500
- function resolveTarget(client, workspaceRoot) {
4501
- const serverName = resolveServerName(workspaceRoot);
4502
- if (client === "codex") return {
4503
- client,
4504
- configPath: path.join(os.homedir(), ".codex", "config.toml"),
4505
- displayName: "Codex",
4506
- serverName
4507
- };
4508
- if (client === "claude-code") return {
4509
- client,
4510
- configPath: path.join(workspaceRoot, ".mcp.json"),
4511
- displayName: "Claude Code",
4512
- serverName
4513
- };
4514
- return {
4515
- client,
4516
- configPath: path.join(workspaceRoot, ".cursor", "mcp.json"),
4517
- displayName: "Cursor",
4518
- serverName
4519
- };
4520
- }
4521
- //#endregion
4522
- //#region src/cli/mcpClient/inspect.ts
4523
- async function probeHttpEndpoint(url) {
4524
- const controller = new AbortController();
4525
- const timer = setTimeout$1(() => {
4526
- controller.abort();
4527
- }, DEFAULT_HTTP_TIMEOUT_MS);
4528
- try {
4529
- return (await fetch(url, {
4530
- method: "GET",
4531
- signal: controller.signal
4532
- })).status > 0;
4533
- } catch {
4534
- return false;
4535
- } finally {
4536
- clearTimeout$1(timer);
4537
- controller.abort();
4538
- }
4539
- }
4540
- async function inspectJsonConfig(target) {
4541
- if (!await fs$1.pathExists(target.configPath)) return {
4542
- configExists: false,
4543
- configPath: target.configPath,
4544
- displayName: target.displayName,
4545
- issues: [`未找到配置文件:${target.configPath}`],
4546
- serverName: target.serverName
4547
- };
4548
- const entry = parseJsonConfig(await fs$1.readFile(target.configPath, "utf8"), target.configPath).mcpServers?.[target.serverName];
4549
- if (!entry) return {
4550
- configExists: true,
4551
- configPath: target.configPath,
4552
- displayName: target.displayName,
4553
- issues: [`配置文件中未找到服务器 ${target.serverName}`],
4554
- serverName: target.serverName
4555
- };
4556
- const transport = entry.url ? "http" : "command";
4557
- const issues = [];
4558
- let httpReachable;
4559
- if (transport === "command") {
4560
- const normalizedBin = (entry.args?.[0])?.replaceAll(WORKSPACE_FOLDER_TOKEN, resolveTargetWorkspaceRoot(target));
4561
- if (!normalizedBin || !await fs$1.pathExists(normalizedBin)) issues.push(`未找到本地 CLI 入口:${normalizedBin ?? "缺少 args[0]"}`);
4562
- } else if (entry.url) {
4563
- httpReachable = await probeHttpEndpoint(entry.url);
4564
- if (!httpReachable) issues.push(`HTTP MCP 地址不可达:${entry.url}`);
4565
- }
4566
- return {
4567
- configExists: true,
4568
- configPath: target.configPath,
4569
- displayName: target.displayName,
4570
- httpReachable,
4571
- issues,
4572
- serverName: target.serverName,
4573
- transport
4574
- };
4575
- }
4576
- async function inspectCodexConfig(target) {
4577
- if (!await fs$1.pathExists(target.configPath)) return {
4578
- configExists: false,
4579
- configPath: target.configPath,
4580
- displayName: target.displayName,
4581
- issues: [`未找到配置文件:${target.configPath}`],
4582
- serverName: target.serverName
4583
- };
4584
- const blockMatch = (await fs$1.readFile(target.configPath, "utf8")).match(resolveCodexBlockPattern(target.serverName));
4585
- if (!blockMatch?.[0]) return {
4586
- configExists: true,
4587
- configPath: target.configPath,
4588
- displayName: target.displayName,
4589
- issues: [`未找到 weapp-vite 生成的 Codex 配置区块:${target.serverName}`],
4590
- serverName: target.serverName
4591
- };
4592
- const block = blockMatch[0];
4593
- const issues = [];
4594
- const urlMatch = block.match(REG_CodexUrl);
4595
- const commandMatch = block.match(REG_CodexCommand);
4596
- const argsMatch = block.match(REG_CodexArgs);
4597
- if (urlMatch?.[1]) {
4598
- const reachable = await probeHttpEndpoint(urlMatch[1]);
4599
- if (!reachable) issues.push(`HTTP MCP 地址不可达:${urlMatch[1]}`);
4600
- return {
4601
- configExists: true,
4602
- configPath: target.configPath,
4603
- displayName: target.displayName,
4604
- httpReachable: reachable,
4605
- issues,
4606
- serverName: target.serverName,
4607
- transport: "http"
4608
- };
4609
- }
4610
- const binPath = (argsMatch?.[1]?.match(REG_FirstTomlString))?.[1];
4611
- if (!commandMatch?.[1]) issues.push("Codex 配置区块缺少 command 字段");
4612
- if (!binPath || !await fs$1.pathExists(binPath)) issues.push(`未找到本地 CLI 入口:${binPath ?? "缺少 args[0]"}`);
4613
- return {
4614
- configExists: true,
4615
- configPath: target.configPath,
4616
- displayName: target.displayName,
4617
- issues,
4618
- serverName: target.serverName,
4619
- transport: "command"
4620
- };
4621
- }
4622
- async function inspectMcpClientConfig(options) {
4623
- const target = resolveTarget(options.client, path.resolve(options.workspaceRoot));
4624
- if (target.client === "codex") return await inspectCodexConfig(target);
4625
- return await inspectJsonConfig(target);
4626
- }
4627
- //#endregion
4628
- //#region src/cli/mcpClient/plan.ts
4629
- function resolveSupportedMcpClient(input) {
4630
- if (input === "codex" || input === "claude-code" || input === "cursor") return input;
4631
- throw new Error(`不支持的 MCP 客户端:${input}。当前仅支持 codex、claude-code、cursor。`);
4632
- }
4633
- function buildMcpClientConfigPlan(options) {
4634
- const workspaceRoot = path.resolve(options.workspaceRoot);
4635
- const target = resolveTarget(options.client, workspaceRoot);
4636
- const entry = resolveConfigEntry(options.client, options.transport, workspaceRoot, options.url);
4637
- if (options.transport === "http" && !entry.url) throw new Error("HTTP 模式缺少 MCP 服务地址,请使用 --url 指定或在当前项目中检测到可用地址。");
4638
- return {
4639
- entry,
4640
- preview: target.client === "codex" ? renderCodexBlock(target.serverName, entry) : renderJsonPreview(target.serverName, entry),
4641
- target,
4642
- transport: options.transport
4643
- };
4644
- }
4645
- function formatMcpQuickStart(options) {
4646
- const suffix = options.transport === "http" && options.httpUrl ? ` --transport http --url ${options.httpUrl}` : "";
4647
- return [
4648
- "在 AI 工具中接入 weapp-vite MCP:",
4649
- ` - Codex: wv mcp init codex${suffix}`,
4650
- ` - Claude Code: wv mcp init claude-code${suffix}`,
4651
- ` - Cursor: wv mcp init cursor${suffix}`
4652
- ];
4653
- }
4654
- //#endregion
4655
- //#region src/cli/mcpClient/index.ts
4656
- async function writeMcpClientConfig(plan) {
4657
- await fs$1.ensureDir(path.dirname(plan.target.configPath));
4658
- const existing = await fs$1.readFile(plan.target.configPath, "utf8").catch(() => "");
4659
- if (plan.target.client === "codex") {
4660
- const nextContent = upsertCodexManagedBlock(existing, plan.target.serverName, plan.preview);
4661
- await fs$1.writeFile(plan.target.configPath, nextContent, "utf8");
4662
- return;
4663
- }
4664
- const parsed = parseJsonConfig(existing, plan.target.configPath);
4665
- const nextConfig = {
4666
- ...parsed,
4667
- mcpServers: {
4668
- ...parsed.mcpServers ?? {},
4669
- [plan.target.serverName]: plan.entry
4670
- }
4671
- };
4672
- await fs$1.writeFile(plan.target.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
4673
- }
4674
- //#endregion
4675
- //#region src/cli/commands/mcp.ts
4676
- function resolvePort(port) {
4677
- if (typeof port === "number" && Number.isInteger(port)) return port;
4678
- if (typeof port === "string" && port.trim()) {
4679
- const parsed = Number.parseInt(port, 10);
4680
- if (Number.isInteger(parsed)) return parsed;
4681
- }
4682
- }
4683
- async function resolveHttpUrl(options) {
4684
- if (options.url?.trim()) return options.url.trim();
4685
- const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
4686
- const originalCwd = process.cwd();
4687
- try {
4688
- if (workspaceRoot !== originalCwd) process.chdir(workspaceRoot);
4689
- const loaded = await loadConfig(resolveConfigFile(options));
4690
- const resolved = resolveWeappMcpConfig(loaded?.config?.weapp?.mcp);
4691
- return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
4692
- } catch {
4693
- const resolved = resolveWeappMcpConfig(void 0);
4694
- return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
4695
- } finally {
4696
- if (process.cwd() !== originalCwd) process.chdir(originalCwd);
4697
- }
4698
- }
4699
- async function confirmWrite() {
4700
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
4701
- return await runWithSuspendedSharedInput(async () => {
4702
- const rl = createInterface({
4703
- input: process.stdin,
4704
- output: process.stdout
4705
- });
4706
- try {
4707
- const normalized = (await rl.question("是否写入配置文件?(Y/n) ")).trim().toLowerCase();
4708
- return normalized === "" || normalized === "y" || normalized === "yes";
4709
- } finally {
4710
- rl.close();
4711
- }
4712
- });
4713
- }
4714
- function resolveClientTransport(transport) {
4715
- return transport === "http" ? "http" : "command";
4716
- }
4717
- async function handlePrint(clientName, options, write = false) {
4718
- const client = resolveSupportedMcpClient(clientName);
4719
- const workspaceRoot = options.workspaceRoot ?? process.cwd();
4720
- const transport = resolveClientTransport(options.transport);
4721
- const plan = buildMcpClientConfigPlan({
4722
- client,
4723
- transport,
4724
- url: transport === "http" ? await resolveHttpUrl(options) : void 0,
4725
- workspaceRoot
4726
- });
4727
- logger_default.info(`${plan.target.displayName} 配置文件:${plan.target.configPath}`);
4728
- logger_default.info(`服务器名称:${plan.target.serverName}`);
4729
- process.stdout.write(`${plan.preview}\n`);
4730
- if (!write) return;
4731
- if (options.yes || await confirmWrite()) {
4732
- await writeMcpClientConfig(plan);
4733
- logger_default.success(`已写入 ${plan.target.displayName} MCP 配置。`);
4734
- logger_default.info(`请重启 ${plan.target.displayName},然后执行:wv mcp doctor ${client}`);
4735
- return;
4736
- }
4737
- logger_default.info("已取消写入。");
4738
- }
4739
- async function handleDoctor(clientName, options) {
4740
- const result = await inspectMcpClientConfig({
4741
- client: resolveSupportedMcpClient(clientName),
4742
- workspaceRoot: options.workspaceRoot ?? process.cwd()
4743
- });
4744
- logger_default.info(`${result.displayName} 配置文件:${result.configPath}`);
4745
- logger_default.info(`服务器名称:${result.serverName}`);
4746
- if (!result.configExists || result.issues.length > 0) {
4747
- for (const issue of result.issues) logger_default.warn(issue);
4748
- throw new Error("MCP 客户端配置检查未通过。");
4749
- }
4750
- if (result.transport) logger_default.info(`传输模式:${result.transport}`);
4751
- if (result.httpReachable !== void 0) logger_default.info(`HTTP 服务可达:${result.httpReachable ? "是" : "否"}`);
4752
- logger_default.success("MCP 客户端配置检查通过。");
4753
- }
4754
- async function handleServer(options) {
4755
- const resolvedTransport = options.transport === "http" ? "streamable-http" : options.transport === "command" ? void 0 : options.transport;
4756
- await startWeappViteMcpServer({
4757
- endpoint: options.endpoint,
4758
- host: options.host,
4759
- port: resolvePort(options.port),
4760
- restEndpoint: options.rest === false ? false : options.restEndpoint,
4761
- transport: resolvedTransport,
4762
- unref: options.unref,
4763
- workspaceRoot: options.workspaceRoot
4764
- });
4765
- for (const line of formatMcpQuickStart({
4766
- httpUrl: resolvedTransport === "streamable-http" ? `http://${options.host ?? "127.0.0.1"}:${resolvePort(options.port) ?? 3088}${options.endpoint ?? "/mcp"}` : void 0,
4767
- transport: resolvedTransport === "streamable-http" ? "http" : "command"
4768
- })) logger_default.info(line);
4769
- }
4770
- function registerMcpCommand(cli) {
4771
- cli.command("mcp [...args]", "start weapp-vite MCP server or manage MCP client onboarding").option("--transport <type>", "[string] stdio | streamable-http | command | http", { default: "stdio" }).option("--host <host>", "[string] streamable-http host").option("--port <port>", "[number] streamable-http port").option("--endpoint <path>", "[string] streamable-http endpoint path").option("--rest-endpoint <path>", "[string] streamable-http REST runtime endpoint path").option("--no-rest", "[boolean] disable streamable-http REST runtime endpoints").option("--unref", "[boolean] unref HTTP server to not block process exit").option("--url <url>", "[string] explicit HTTP MCP url").option("--workspace-root <path>", "[string] workspace root path, defaults to cwd").option("-y, --yes", "[boolean] write config without prompt").action(async (args, options) => {
4772
- const [subcommand, client] = args;
4773
- if (subcommand === "init") {
4774
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp init <codex|claude-code|cursor>");
4775
- await handlePrint(client, options, true);
4776
- return;
4777
- }
4778
- if (subcommand === "print") {
4779
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp print <codex|claude-code|cursor>");
4780
- await handlePrint(client, options, false);
4781
- return;
4782
- }
4783
- if (subcommand === "doctor") {
4784
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp doctor <codex|claude-code|cursor>");
4785
- await handleDoctor(client, options);
4786
- return;
4787
- }
4788
- if (subcommand) throw new Error(`未知的 mcp 子命令:${subcommand}`);
4789
- await handleServer(options);
4790
- });
4791
- }
4792
- //#endregion
4793
4575
  //#region src/cli/commands/npm.ts
4794
4576
  function registerNpmCommand(cli) {
4795
4577
  cli.command("npm").alias("build:npm").alias("build-npm").action(async () => {
@@ -4911,7 +4693,7 @@ function applyMcpCliOptions(config, options) {
4911
4693
  //#endregion
4912
4694
  //#region src/cli/commands/open.ts
4913
4695
  function registerOpenCommand(cli) {
4914
- cli.command("open [root]").option("-p, --platform <platform>", `[string] target platform (weapp | web)`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--login-retry <mode>", "[string] login retry mode for Wechat DevTools (never | once | always)").option("--login-retry-timeout <ms>", "[number] login retry prompt timeout in milliseconds").option("--non-interactive", "[boolean] fail immediately when Wechat DevTools login has expired").option("--no-open-recovery", "[boolean] disable automatic Wechat DevTools close-and-reopen recovery").option("--mcp", "[boolean] auto start MCP service before opening IDE").option("--no-mcp", "[boolean] disable MCP service before opening IDE").action(async (root, options) => {
4696
+ cli.command("open [root]").option("-p, --platform <platform>", `[string] target platform (weapp | web)`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--login-retry <mode>", "[string] login retry mode for Wechat DevTools (never | once | always)").option("--login-retry-timeout <ms>", "[number] login retry prompt timeout in milliseconds").option("--non-interactive", "[boolean] fail immediately when Wechat DevTools login has expired").option("--no-open-recovery", "[boolean] disable automatic target-project open retry (preserves existing IDE windows)").option("--mcp", "[boolean] auto start MCP service before opening IDE").option("--no-mcp", "[boolean] disable MCP service before opening IDE").action(async (root, options) => {
4915
4697
  filterDuplicateOptions(options);
4916
4698
  const configFile = resolveConfigFile(options);
4917
4699
  const targets = resolveRuntimeTargets(options);
@@ -5214,9 +4996,6 @@ function resolveRunnableHotkeyDefinition(input) {
5214
4996
  return DEV_HOTKEY_DEFINITIONS.find((action) => action.run && action.key === input);
5215
4997
  }
5216
4998
  //#endregion
5217
- //#region package.json
5218
- var version = "7.4.0";
5219
- //#endregion
5220
4999
  //#region src/cli/devHotkeys/format.ts
5221
5000
  const FULLWIDTH_ASCII_START = 65281;
5222
5001
  const FULLWIDTH_ASCII_END = 65374;
@@ -5376,6 +5155,8 @@ function startDevHotkeys(options) {
5376
5155
  if (options.platform !== "weapp" || !process.stdin.isTTY) return;
5377
5156
  let closed = false;
5378
5157
  let running = false;
5158
+ let closing;
5159
+ let actionPromise;
5379
5160
  let mcpHandle;
5380
5161
  let onData;
5381
5162
  let onKeypress;
@@ -5410,17 +5191,25 @@ function startDevHotkeys(options) {
5410
5191
  }
5411
5192
  });
5412
5193
  const close = () => {
5413
- if (closed) return;
5194
+ if (closing) return closing;
5414
5195
  closed = true;
5415
5196
  if (onSigcont) process.off("SIGCONT", onSigcont);
5416
- inputSession?.close();
5197
+ const inputErrors = [];
5198
+ try {
5199
+ inputSession?.close();
5200
+ } catch (error) {
5201
+ inputErrors.push(error);
5202
+ }
5417
5203
  inputSession = void 0;
5418
- if (mcpHandle?.close) mcpHandle.close().catch((error) => {
5419
- logger_default.warn(`[dev action] MCP 服务关闭失败:${error instanceof Error ? error.message : String(error)}`);
5420
- });
5421
- closeSharedMiniProgram(options.projectPath).catch((error) => {
5422
- logger_default.warn(`[dev action] DevTools 会话关闭失败:${error instanceof Error ? error.message : String(error)}`);
5423
- });
5204
+ return closing = (async () => {
5205
+ await actionPromise;
5206
+ const handle = mcpHandle;
5207
+ const closeHandle = handle?.close?.bind(handle);
5208
+ mcpHandle = void 0;
5209
+ const results = await Promise.allSettled([Promise.resolve().then(() => closeHandle?.()), Promise.resolve().then(() => closeSharedMiniProgram(options.projectPath))]);
5210
+ const failures = [...inputErrors, ...results.filter((result) => result.status === "rejected").map((result) => result.reason)];
5211
+ if (failures.length) throw new AggregateError(failures, "开发快捷键资源关闭失败");
5212
+ })();
5424
5213
  };
5425
5214
  const printPanel = (message, force = false) => {
5426
5215
  if (!force && message === lastRenderedPanel) return;
@@ -5458,8 +5247,9 @@ function startDevHotkeys(options) {
5458
5247
  const resolvePendingLabel = (input) => {
5459
5248
  if (input === "m") return mcpHandle?.close ? "正在关闭 MCP 服务" : "正在启动 MCP 服务";
5460
5249
  };
5461
- const runAction = (label, pendingLabel, action) => {
5250
+ const runAction = (label, pendingLabel, action, quiet = false) => {
5462
5251
  if (running) {
5252
+ if (quiet) return;
5463
5253
  const current = currentAction ?? "已有命令";
5464
5254
  const elapsed = actionStartedAt === void 0 ? "" : `(已运行 ${Math.max(0, Date.now() - actionStartedAt)} ms)`;
5465
5255
  logger_default.warn(`[dev action] 当前正在${current.replace(REG_PENDING_PREFIX, "")}${elapsed},已忽略重复请求。`);
@@ -5468,24 +5258,8 @@ function startDevHotkeys(options) {
5468
5258
  running = true;
5469
5259
  actionStartedAt = Date.now();
5470
5260
  currentAction = pendingLabel;
5471
- printHint();
5472
- action().then((summary) => {
5473
- if (summary) lastAction = summary;
5474
- }).catch((error) => {
5475
- logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
5476
- }).finally(() => {
5477
- running = false;
5478
- actionStartedAt = void 0;
5479
- currentAction = void 0;
5480
- if (!closed) printHint();
5481
- });
5482
- };
5483
- const runBackgroundAction = (label, pendingLabel, action) => {
5484
- if (running) return;
5485
- running = true;
5486
- actionStartedAt = Date.now();
5487
- currentAction = pendingLabel;
5488
- action().then((summary) => {
5261
+ if (!quiet) printHint();
5262
+ actionPromise = action().then((summary) => {
5489
5263
  if (summary) lastAction = summary;
5490
5264
  }).catch((error) => {
5491
5265
  logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
@@ -5493,13 +5267,14 @@ function startDevHotkeys(options) {
5493
5267
  running = false;
5494
5268
  actionStartedAt = void 0;
5495
5269
  currentAction = void 0;
5270
+ if (!closed && !quiet) printHint();
5496
5271
  });
5497
5272
  };
5498
5273
  const handleInput = (input) => {
5499
5274
  if (closed) return;
5500
5275
  const normalizedInput = normalizeInputChar(input);
5501
5276
  if (normalizedInput === "") {
5502
- close();
5277
+ close().catch((error) => logger_default.error(error));
5503
5278
  forwardSigint();
5504
5279
  return;
5505
5280
  }
@@ -5509,7 +5284,7 @@ function startDevHotkeys(options) {
5509
5284
  }
5510
5285
  const normalized = normalizedInput.toLowerCase();
5511
5286
  if (normalized === "q") {
5512
- close();
5287
+ close().catch((error) => logger_default.error(error));
5513
5288
  forwardSigint();
5514
5289
  return;
5515
5290
  }
@@ -5559,9 +5334,9 @@ function startDevHotkeys(options) {
5559
5334
  };
5560
5335
  process.on("SIGCONT", onSigcont);
5561
5336
  if (!options.silentStartupHint) printHint();
5562
- if (resolvedMcp.enabled && resolvedMcp.autoStart) runBackgroundAction("MCP 自动启动", "正在启动 MCP 服务", async () => {
5337
+ if (resolvedMcp.enabled && resolvedMcp.autoStart) runAction("MCP 自动启动", "正在启动 MCP 服务", async () => {
5563
5338
  await toggleMcp({ silent: true });
5564
- });
5339
+ }, true);
5565
5340
  return {
5566
5341
  close,
5567
5342
  restore,
@@ -5569,6 +5344,127 @@ function startDevHotkeys(options) {
5569
5344
  };
5570
5345
  }
5571
5346
  //#endregion
5347
+ //#region src/cli/commands/serve/shared.ts
5348
+ function startBackgroundForwardConsole(startForwardConsole, options) {
5349
+ if (!startForwardConsole) return;
5350
+ try {
5351
+ startForwardConsole(options).catch((error) => {
5352
+ const message = error instanceof Error ? error.message : String(error);
5353
+ logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
5354
+ });
5355
+ } catch (error) {
5356
+ const message = error instanceof Error ? error.message : String(error);
5357
+ logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
5358
+ }
5359
+ }
5360
+ function resolveServeIdeOpenStrategy(options = {}, fallback = "cli") {
5361
+ return options.openStrategy ?? (options.useAutomatorOpen ? "automator" : fallback);
5362
+ }
5363
+ function resolveWebHost(host) {
5364
+ if (host === void 0) return;
5365
+ if (typeof host === "boolean") return host;
5366
+ if (typeof host === "string") return host;
5367
+ return String(host);
5368
+ }
5369
+ /**
5370
+ * @description 为 serve 模式构造统一的小程序开发动作,供热键与命令复用。
5371
+ */
5372
+ function createServeMiniProgramDevActions(options) {
5373
+ const projectPath = options.projectPath ?? options.fallbackProjectPath;
5374
+ return {
5375
+ projectPath,
5376
+ openIde: async (openOptions = {}) => {
5377
+ if (!openOptions.forceOpen && await options.tryReuseForwardConsole?.()) return "已通过控制台转发复用当前开发者工具会话";
5378
+ await options.openIde(projectPath, openOptions);
5379
+ startBackgroundForwardConsole(options.startForwardConsole, openOptions);
5380
+ return openOptions.forceReopen ? "已重新打开微信开发者工具项目" : "已打开或复用微信开发者工具项目";
5381
+ },
5382
+ rebuild: async () => {
5383
+ await options.build();
5384
+ return "已手动重新构建当前小程序产物";
5385
+ }
5386
+ };
5387
+ }
5388
+ //#endregion
5389
+ //#region src/cli/commands/serve/actions.ts
5390
+ /** 启动与热键共用动作,退出后不再创建新的 IDE 或日志消费者。 */
5391
+ function createServeDevelopmentActions(ctx, miniBackend, options, shutdown) {
5392
+ const { configService } = ctx;
5393
+ const generatedProjectConfig = configService.multiPlatform.projectConfigs !== void 0;
5394
+ return createServeMiniProgramDevActions({
5395
+ build: async () => {
5396
+ if (!shutdown.stopping) await shutdown.run("startup", () => miniBackend?.driver.dev(ctx, options));
5397
+ },
5398
+ fallbackProjectPath: configService.cwd,
5399
+ openIde: async (projectPath, openOptions) => {
5400
+ if (shutdown.stopping) return;
5401
+ const forceReopen = openOptions?.forceReopen === true;
5402
+ const openStrategy = resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy);
5403
+ const useAutomatorOpen = openStrategy === "automator";
5404
+ await openIde(configService.platform, projectPath, {
5405
+ loginRetry: options.loginRetry,
5406
+ loginRetryTimeout: options.loginRetryTimeout,
5407
+ nonInteractive: options.nonInteractive,
5408
+ openRecovery: false,
5409
+ prepareAutomatorSession: useAutomatorOpen,
5410
+ reuseOpenedProject: !forceReopen,
5411
+ skipAutomatorCompile: !forceReopen,
5412
+ skipPostOpenHealthCheck: true,
5413
+ trustProject: options.trustProject,
5414
+ openStrategy,
5415
+ useAutomatorOpen
5416
+ });
5417
+ process.stdout?.write?.("\n");
5418
+ },
5419
+ projectPath: resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd, generatedProjectConfig),
5420
+ startForwardConsole: async (openOptions) => {
5421
+ if (shutdown.stopping) return false;
5422
+ if (resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy) === "automator") return await maybeStartForwardConsole({
5423
+ openedOnly: true,
5424
+ preferOpenedSession: true,
5425
+ platform: configService.platform,
5426
+ mpDistRoot: configService.mpDistRoot,
5427
+ cwd: configService.cwd,
5428
+ weappViteConfig: configService.weappViteConfig
5429
+ });
5430
+ return await maybeStartForwardConsole({
5431
+ platform: configService.platform,
5432
+ mpDistRoot: configService.mpDistRoot,
5433
+ cwd: configService.cwd,
5434
+ preferOpenedSession: false,
5435
+ recoverAutomatorSession: async () => {
5436
+ if (shutdown.stopping) return;
5437
+ const projectPath = resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd, generatedProjectConfig);
5438
+ await openIde(configService.platform, projectPath, {
5439
+ loginRetry: options.loginRetry,
5440
+ loginRetryTimeout: options.loginRetryTimeout,
5441
+ nonInteractive: options.nonInteractive,
5442
+ openRecovery: false,
5443
+ openStrategy: "automator",
5444
+ prepareAutomatorSession: true,
5445
+ reuseOpenedProject: false,
5446
+ skipAutomatorCompile: false,
5447
+ skipPostOpenHealthCheck: true,
5448
+ trustProject: options.trustProject,
5449
+ useAutomatorOpen: true
5450
+ });
5451
+ },
5452
+ weappViteConfig: configService.weappViteConfig
5453
+ });
5454
+ },
5455
+ tryReuseForwardConsole: async () => {
5456
+ if (shutdown.stopping) return false;
5457
+ return await maybeStartForwardConsole({
5458
+ openedOnly: true,
5459
+ platform: configService.platform,
5460
+ mpDistRoot: configService.mpDistRoot,
5461
+ cwd: configService.cwd,
5462
+ weappViteConfig: configService.weappViteConfig
5463
+ });
5464
+ }
5465
+ });
5466
+ }
5467
+ //#endregion
5572
5468
  //#region src/cli/commands/serve/analyze.ts
5573
5469
  const REG_DIST_PAGE_ENTRY = /pages\/.+\/index\.js$/;
5574
5470
  const REG_DIST_POSIX_SEP = /\\/g;
@@ -5683,7 +5579,7 @@ async function analyzeUiFallback(ctx, options) {
5683
5579
  };
5684
5580
  }
5685
5581
  function createAnalyzeController(options) {
5686
- const { configFile, ctx, options: cliOptions, targets } = options;
5582
+ const { configFile, ctx, options: cliOptions, targets, shutdown } = options;
5687
5583
  const { configService } = ctx;
5688
5584
  let analyzeRunId = 0;
5689
5585
  let analyzeHandle;
@@ -5734,8 +5630,8 @@ function createAnalyzeController(options) {
5734
5630
  }
5735
5631
  return runFallback("完整分析结果为空,已回退到 dist 文件扫描。");
5736
5632
  };
5737
- const triggerAnalyzeUpdate = async (reason = "watch") => {
5738
- if (!analyzeHandle) return;
5633
+ const runAnalyzeUpdate = async (reason) => {
5634
+ if (!analyzeHandle || shutdown?.stopping) return;
5739
5635
  emitDashboardEvents(analyzeHandle, [{
5740
5636
  kind: reason === "watch" ? "hmr" : "build",
5741
5637
  level: "info",
@@ -5744,6 +5640,7 @@ function createAnalyzeController(options) {
5744
5640
  tags: reason === "watch" ? ["watch", "analyze"] : ["initial", "analyze"]
5745
5641
  }]);
5746
5642
  const next = await runAnalyze();
5643
+ if (shutdown?.stopping) return;
5747
5644
  if (next.mode === "fallback") emitDashboardEvents(analyzeHandle, [{
5748
5645
  kind: "diagnostic",
5749
5646
  level: "warning",
@@ -5762,6 +5659,10 @@ function createAnalyzeController(options) {
5762
5659
  tags: next.mode === "fallback" ? ["analyze", "fallback"] : ["analyze", reason === "watch" ? "refresh" : "initial"]
5763
5660
  }]);
5764
5661
  };
5662
+ const triggerAnalyzeUpdate = (reason = "watch") => {
5663
+ if (shutdown?.stopping) return Promise.resolve();
5664
+ return shutdown ? shutdown.run("startup", () => runAnalyzeUpdate(reason)) : runAnalyzeUpdate(reason);
5665
+ };
5765
5666
  const bindWatcher = (buildResult) => {
5766
5667
  let initialUpdatePromise;
5767
5668
  let watchUpdatePending = false;
@@ -5791,6 +5692,7 @@ function createAnalyzeController(options) {
5791
5692
  return watchUpdatePromise;
5792
5693
  };
5793
5694
  if (analyzeHandle && buildResult && typeof buildResult.on === "function") buildResult.on("event", (event) => {
5695
+ if (shutdown?.stopping) return;
5794
5696
  if (event.code === "ERROR") {
5795
5697
  emitDashboardEvents(analyzeHandle, [{
5796
5698
  kind: "diagnostic",
@@ -5814,8 +5716,10 @@ function createAnalyzeController(options) {
5814
5716
  getHandle: () => analyzeHandle,
5815
5717
  async startDashboard(startDashboard) {
5816
5718
  const initialAnalyze = await runAnalyze();
5719
+ if (shutdown?.stopping) return;
5817
5720
  analyzeHandle = await startDashboard(initialAnalyze.result, {
5818
5721
  watch: true,
5722
+ uiHost: cliOptions.uiHost,
5819
5723
  artifacts: initialAnalyze.artifacts,
5820
5724
  cwd: configService.cwd,
5821
5725
  packageManagerAgent: configService.packageManager.agent,
@@ -5867,279 +5771,185 @@ function createAnalyzeController(options) {
5867
5771
  };
5868
5772
  }
5869
5773
  //#endregion
5870
- //#region src/cli/commands/serve/shared.ts
5871
- function startBackgroundForwardConsole(startForwardConsole, options) {
5872
- if (!startForwardConsole) return;
5873
- try {
5874
- startForwardConsole(options).catch((error) => {
5875
- const message = error instanceof Error ? error.message : String(error);
5876
- logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
5774
+ //#region src/cli/commands/serve/startup.ts
5775
+ /** 启动阶段只取得资源;退出协调器负责等待在途操作并统一释放。 */
5776
+ async function startServeCommand(root, options, shutdown) {
5777
+ if (shutdown.stopping) return;
5778
+ filterDuplicateOptions(options);
5779
+ options.uiHost = parseDashboardUiHost(options.uiHost);
5780
+ setCommandNodeEnv("development");
5781
+ const cwd = root ?? process.cwd();
5782
+ const configFile = resolveConfigFile(options);
5783
+ let targets = resolveRuntimeTargets(options);
5784
+ const host = resolveWebHost(options.host);
5785
+ const inlineConfig = createInlineConfig(targets, {
5786
+ scope: options.scope,
5787
+ host,
5788
+ inlineConfig: host === void 0 ? void 0 : { server: { host } }
5789
+ });
5790
+ const ctx = await createCompilerContext({
5791
+ cwd,
5792
+ mode: options.mode ?? "development",
5793
+ isDev: true,
5794
+ configFile,
5795
+ inlineConfig,
5796
+ cliPlatform: targets.rawPlatform,
5797
+ preloadAppEntry: false,
5798
+ syncAutoImportSupportFiles: false,
5799
+ projectConfigPath: options.projectConfig
5800
+ });
5801
+ const { configService } = ctx;
5802
+ targets = resolveConfiguredRuntimeTargets(targets, configService.options?.sourceConfig?.weapp?.platform);
5803
+ for (const backend of targets.select("dev")) shutdown.own(() => backend.driver.close(ctx));
5804
+ if (shutdown.stopping) return;
5805
+ const webBackend = getBackendForCapability(targets, "web", "dev");
5806
+ const miniBackend = getBackendForCapability(targets, "miniprogram", "dev");
5807
+ const aiEnvironment = await detectAiDevelopmentEnvironment();
5808
+ if (shutdown.stopping) return;
5809
+ const mcpConfig = applyMcpCliOptions(configService.weappViteConfig?.mcp, options);
5810
+ logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
5811
+ const enableAnalyze = Boolean(isUiEnabled(options) && miniBackend);
5812
+ let analyzeHandle;
5813
+ const miniProgramDevActions = createServeDevelopmentActions(ctx, miniBackend, options, shutdown);
5814
+ const devHotkeysSession = miniBackend ? startDevHotkeys({
5815
+ cwd: configService.cwd,
5816
+ agentName: aiEnvironment.agentName,
5817
+ isAgent: aiEnvironment.isAgent,
5818
+ mcpConfig,
5819
+ openIde: async () => await miniProgramDevActions.openIde({
5820
+ forceOpen: true,
5821
+ forceReopen: true
5822
+ }),
5823
+ platform: configService.platform,
5824
+ projectPath: miniProgramDevActions.projectPath ?? configService.cwd,
5825
+ rebuild: miniProgramDevActions.rebuild,
5826
+ silentStartupHint: true,
5827
+ weappViteConfig: configService.weappViteConfig
5828
+ }) : void 0;
5829
+ shutdown.own(() => devHotkeysSession?.close());
5830
+ shutdown.own(closeActiveForwardConsole);
5831
+ shutdown.own(() => analyzeHandle?.close());
5832
+ const analyzeController = createAnalyzeController({
5833
+ configFile,
5834
+ ctx,
5835
+ options,
5836
+ targets,
5837
+ shutdown
5838
+ });
5839
+ let webServer;
5840
+ for (const backend of targets.select("dev")) {
5841
+ if (shutdown.stopping) return;
5842
+ if (backend.descriptor.id === "miniprogram") {
5843
+ const miniBuildStartedAt = Date.now();
5844
+ const buildResult = await backend.driver.dev(ctx, options);
5845
+ if (shutdown.stopping) return;
5846
+ const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
5847
+ logger_default.success(`小程序初次构建完成,耗时:${formatDuration(miniBuildDurationMs)}`);
5848
+ shutdown.run("startup", async () => {
5849
+ await ctx.autoImportService?.syncSupportFileResolverComponents().catch((error) => {
5850
+ const message = error instanceof Error ? error.message : String(error);
5851
+ logger_default.warn(`[prepare] 后台同步 .weapp-vite 支持文件失败:${message}`);
5852
+ });
5853
+ });
5854
+ if (enableAnalyze) {
5855
+ await analyzeController.startDashboard(startAnalyzeDashboard);
5856
+ analyzeHandle = analyzeController.getHandle();
5857
+ if (shutdown.stopping) return;
5858
+ analyzeController.emitRuntimeEvents([{
5859
+ kind: "build",
5860
+ level: "success",
5861
+ title: "mini initial build completed",
5862
+ detail: "小程序开发态初次构建已完成,dashboard 可继续监听后续刷新。",
5863
+ durationMs: miniBuildDurationMs,
5864
+ tags: [
5865
+ "dev",
5866
+ "mini",
5867
+ "initial"
5868
+ ]
5869
+ }]);
5870
+ await analyzeController.bindWatcher(buildResult).runInitialUpdate();
5871
+ if (shutdown.stopping) return;
5872
+ }
5873
+ continue;
5874
+ }
5875
+ if (backend.descriptor.id !== "web") continue;
5876
+ const webServerStartedAt = Date.now();
5877
+ try {
5878
+ webServer = await backend.driver.dev(ctx, options);
5879
+ if (shutdown.stopping) return;
5880
+ logger_default.success(`Web 开发服务启动完成,耗时:${formatDuration(Date.now() - webServerStartedAt)}`);
5881
+ analyzeController.emitRuntimeEvents([{
5882
+ kind: "system",
5883
+ level: "success",
5884
+ title: "web dev server started",
5885
+ detail: "Web 开发服务器已启动,可与小程序调试 UI 并行工作。",
5886
+ durationMs: Date.now() - webServerStartedAt,
5887
+ tags: ["dev", "web"]
5888
+ }]);
5889
+ } catch (error) {
5890
+ analyzeController.emitRuntimeEvents([{
5891
+ kind: "diagnostic",
5892
+ level: "error",
5893
+ title: "web dev server failed",
5894
+ detail: error instanceof Error ? error.message : String(error),
5895
+ durationMs: Date.now() - webServerStartedAt,
5896
+ tags: ["dev", "web"]
5897
+ }]);
5898
+ logger_default.error(error);
5899
+ throw error;
5900
+ }
5901
+ }
5902
+ if (shutdown.stopping) return;
5903
+ if (miniBackend) {
5904
+ logBuildAppFinish(configService, webServer, {
5905
+ skipWeb: !webBackend,
5906
+ uiUrls: analyzeHandle?.urls
5877
5907
  });
5878
- } catch (error) {
5879
- const message = error instanceof Error ? error.message : String(error);
5880
- logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
5908
+ devHotkeysSession?.restore();
5909
+ } else if (webBackend) logBuildAppFinish(configService, webServer, { skipMini: true });
5910
+ if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
5911
+ analyzeController.emitRuntimeEvents([{
5912
+ kind: "command",
5913
+ level: "info",
5914
+ title: "opening ide",
5915
+ detail: "开发服务已就绪,准备打开 IDE 项目。",
5916
+ tags: ["ide", "open"]
5917
+ }]);
5918
+ devHotkeysSession?.suspend();
5919
+ try {
5920
+ await miniProgramDevActions.openIde({
5921
+ forceOpen: true,
5922
+ forceReopen: false,
5923
+ openStrategy: options.ideOpenStrategy ?? "cli"
5924
+ });
5925
+ } finally {
5926
+ devHotkeysSession?.restore();
5927
+ }
5881
5928
  }
5882
- }
5883
- function resolveServeIdeOpenStrategy(options = {}, fallback = "cli") {
5884
- return options.openStrategy ?? (options.useAutomatorOpen ? "automator" : fallback);
5885
- }
5886
- function resolveWebHost(host) {
5887
- if (host === void 0) return;
5888
- if (typeof host === "boolean") return host;
5889
- if (typeof host === "string") return host;
5890
- return String(host);
5891
- }
5892
- /**
5893
- * @description 为 serve 模式构造统一的小程序开发动作,供热键与命令复用。
5894
- */
5895
- function createServeMiniProgramDevActions(options) {
5896
- const projectPath = options.projectPath ?? options.fallbackProjectPath;
5897
5929
  return {
5898
- projectPath,
5899
- openIde: async (openOptions = {}) => {
5900
- if (!openOptions.forceOpen && await options.tryReuseForwardConsole?.()) return "已通过控制台转发复用当前开发者工具会话";
5901
- await options.openIde(projectPath, openOptions);
5902
- startBackgroundForwardConsole(options.startForwardConsole, openOptions);
5903
- return openOptions.forceReopen ? "已重新打开微信开发者工具项目" : "已打开或复用微信开发者工具项目";
5904
- },
5905
- rebuild: async () => {
5906
- await options.build();
5907
- return "已手动重新构建当前小程序产物";
5908
- }
5930
+ watch: targets.has("dev"),
5931
+ waitForExit: analyzeHandle ? () => analyzeController.waitForExit() : void 0
5909
5932
  };
5910
5933
  }
5911
- function waitForServeShutdownSignal() {
5912
- return new Promise((resolve) => {
5913
- const onSignal = () => {
5914
- process.off("SIGINT", onSignal);
5915
- process.off("SIGTERM", onSignal);
5916
- resolve();
5917
- };
5918
- process.on("SIGINT", onSignal);
5919
- process.on("SIGTERM", onSignal);
5920
- });
5921
- }
5922
5934
  //#endregion
5923
5935
  //#region src/cli/commands/serve/index.ts
5924
- function writePostOpenSeparator() {
5925
- process.stdout?.write?.("\n");
5926
- }
5927
5936
  function registerServeCommand(cli) {
5928
- cli.command("[root]", "start dev server").alias("serve").alias("dev").option("--skipNpm", `[boolean] if skip npm build`).option("-o, --open", `[boolean] open ide`).option("-p, --platform <platform>", `[string] target platform (weapp | web | all)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--login-retry <mode>", "[string] login retry mode for Wechat DevTools (never | once | always)").option("--login-retry-timeout <ms>", "[number] login retry prompt timeout in milliseconds").option("--non-interactive", "[boolean] fail immediately when Wechat DevTools login has expired").option("--no-open-recovery", "[boolean] disable automatic Wechat DevTools close-and-reopen recovery").option("--mcp", "[boolean] auto start MCP service during dev").option("--no-mcp", "[boolean] disable MCP service during dev").option("--host [host]", `[string] web dev server host`).option("--ui", `[boolean] 启动调试 UI(当前提供分析视图)`, { default: false }).option("--analyze", `[boolean] 启动分包分析仪表盘 (实验特性)`, { default: false }).option("--scope <scope>", `[string] 局部构建范围,例如 main,packages/order`).action(async (root, options) => {
5929
- filterDuplicateOptions(options);
5930
- setCommandNodeEnv("development");
5931
- const cwd = root ?? process.cwd();
5932
- const configFile = resolveConfigFile(options);
5933
- const targets = resolveRuntimeTargets(options);
5934
- const webBackend = getBackendForCapability(targets, "web", "dev");
5935
- const host = webBackend ? resolveWebHost(options.host) : void 0;
5936
- const inlineConfig = createInlineConfig(targets, {
5937
- scope: options.scope,
5938
- host
5939
- });
5940
- const ctx = await createCompilerContext({
5941
- cwd,
5942
- mode: options.mode ?? "development",
5943
- isDev: true,
5944
- configFile,
5945
- inlineConfig,
5946
- cliPlatform: targets.rawPlatform,
5947
- preloadAppEntry: false,
5948
- syncAutoImportSupportFiles: false,
5949
- projectConfigPath: options.projectConfig
5950
- });
5951
- const { configService } = ctx;
5952
- const miniBackend = getBackendForCapability(targets, "miniprogram", "dev");
5953
- const aiEnvironment = await detectAiDevelopmentEnvironment();
5954
- const mcpConfig = applyMcpCliOptions(configService.weappViteConfig?.mcp, options);
5955
- logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
5956
- const enableAnalyze = Boolean(isUiEnabled(options) && miniBackend);
5957
- let analyzeHandle;
5958
- const generatedProjectConfig = configService.multiPlatform.projectConfigs !== void 0;
5959
- const miniProgramDevActions = createServeMiniProgramDevActions({
5960
- build: async () => {
5961
- await miniBackend?.driver.dev(ctx, options);
5962
- },
5963
- fallbackProjectPath: configService.cwd,
5964
- openIde: async (projectPath, openOptions) => {
5965
- const forceReopen = openOptions?.forceReopen === true;
5966
- const openStrategy = resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy);
5967
- const useAutomatorOpen = openStrategy === "automator";
5968
- await openIde(configService.platform, projectPath, {
5969
- loginRetry: options.loginRetry,
5970
- loginRetryTimeout: options.loginRetryTimeout,
5971
- nonInteractive: options.nonInteractive,
5972
- openRecovery: false,
5973
- prepareAutomatorSession: useAutomatorOpen,
5974
- reuseOpenedProject: !forceReopen,
5975
- skipAutomatorCompile: !forceReopen,
5976
- skipPostOpenHealthCheck: true,
5977
- trustProject: options.trustProject,
5978
- openStrategy,
5979
- useAutomatorOpen
5980
- });
5981
- writePostOpenSeparator();
5982
- },
5983
- projectPath: resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd, generatedProjectConfig),
5984
- startForwardConsole: async (openOptions) => {
5985
- if (resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy) === "automator") return await maybeStartForwardConsole({
5986
- openedOnly: true,
5987
- preferOpenedSession: true,
5988
- platform: configService.platform,
5989
- mpDistRoot: configService.mpDistRoot,
5990
- cwd: configService.cwd,
5991
- weappViteConfig: configService.weappViteConfig
5992
- });
5993
- return await maybeStartForwardConsole({
5994
- platform: configService.platform,
5995
- mpDistRoot: configService.mpDistRoot,
5996
- cwd: configService.cwd,
5997
- preferOpenedSession: false,
5998
- recoverAutomatorSession: async () => {
5999
- const projectPath = resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd, generatedProjectConfig);
6000
- await openIde(configService.platform, projectPath, {
6001
- loginRetry: options.loginRetry,
6002
- loginRetryTimeout: options.loginRetryTimeout,
6003
- nonInteractive: options.nonInteractive,
6004
- openRecovery: false,
6005
- openStrategy: "automator",
6006
- prepareAutomatorSession: true,
6007
- reuseOpenedProject: false,
6008
- skipAutomatorCompile: false,
6009
- skipPostOpenHealthCheck: true,
6010
- trustProject: options.trustProject,
6011
- useAutomatorOpen: true
6012
- });
6013
- },
6014
- weappViteConfig: configService.weappViteConfig
6015
- });
6016
- },
6017
- tryReuseForwardConsole: async () => {
6018
- return await maybeStartForwardConsole({
6019
- openedOnly: true,
6020
- platform: configService.platform,
6021
- mpDistRoot: configService.mpDistRoot,
6022
- cwd: configService.cwd,
6023
- weappViteConfig: configService.weappViteConfig
6024
- });
6025
- }
6026
- });
6027
- const devHotkeysSession = miniBackend ? startDevHotkeys({
6028
- cwd: configService.cwd,
6029
- agentName: aiEnvironment.agentName,
6030
- isAgent: aiEnvironment.isAgent,
6031
- mcpConfig,
6032
- openIde: async () => await miniProgramDevActions.openIde({
6033
- forceOpen: true,
6034
- forceReopen: true
6035
- }),
6036
- platform: configService.platform,
6037
- projectPath: miniProgramDevActions.projectPath ?? configService.cwd,
6038
- rebuild: miniProgramDevActions.rebuild,
6039
- silentStartupHint: true,
6040
- weappViteConfig: configService.weappViteConfig
6041
- }) : void 0;
5937
+ cli.command("[root]", "start dev server").alias("serve").alias("dev").option("--skipNpm", `[boolean] if skip npm build`).option("-o, --open", `[boolean] open ide`).option("-p, --platform <platform>", `[string] target platform (weapp | web | all)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).option("--trust-project", "[boolean] auto trust Wechat DevTools project on open", { default: true }).option("--ide-open-strategy <strategy>", "[string] IDE open strategy (cli | automator)", { default: "cli" }).option("--login-retry <mode>", "[string] login retry mode for Wechat DevTools (never | once | always)").option("--login-retry-timeout <ms>", "[number] login retry prompt timeout in milliseconds").option("--non-interactive", "[boolean] fail immediately when Wechat DevTools login has expired").option("--no-open-recovery", "[boolean] disable automatic target-project open retry (preserves existing IDE windows)").option("--mcp", "[boolean] auto start MCP service during dev").option("--no-mcp", "[boolean] disable MCP service during dev").option("--host [host]", `[string] web dev server host`).option("--ui", `[boolean] 启动调试 UI(当前提供分析视图)`, { default: false }).option("--ui-host <host>", `[string] 启动调试 UI 并选择宿主(standalone | hub)`).option("--analyze", `[boolean] 启动分包分析仪表盘 (实验特性)`, { default: false }).option("--scope <scope>", `[string] 局部构建范围,例如 main,packages/order`).action(async (root, options) => {
5938
+ const shutdown = createDevShutdownScope({ reportError: (error) => logger_default.error(error) });
5939
+ let failure;
6042
5940
  try {
6043
- const analyzeController = createAnalyzeController({
6044
- configFile,
6045
- ctx,
6046
- options,
6047
- targets
6048
- });
6049
- let webServer;
6050
- for (const backend of targets.select("dev")) {
6051
- if (backend.descriptor.id === "miniprogram") {
6052
- const miniBuildStartedAt = Date.now();
6053
- const buildResult = await backend.driver.dev(ctx, options);
6054
- const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
6055
- logger_default.success(`小程序初次构建完成,耗时:${formatDuration(miniBuildDurationMs)}`);
6056
- ctx.autoImportService?.syncSupportFileResolverComponents().catch((error) => {
6057
- const message = error instanceof Error ? error.message : String(error);
6058
- logger_default.warn(`[prepare] 后台同步 .weapp-vite 支持文件失败:${message}`);
6059
- });
6060
- if (enableAnalyze) {
6061
- await analyzeController.startDashboard(startAnalyzeDashboard);
6062
- analyzeHandle = analyzeController.getHandle();
6063
- analyzeController.emitRuntimeEvents([{
6064
- kind: "build",
6065
- level: "success",
6066
- title: "mini initial build completed",
6067
- detail: "小程序开发态初次构建已完成,dashboard 可继续监听后续刷新。",
6068
- durationMs: miniBuildDurationMs,
6069
- tags: [
6070
- "dev",
6071
- "mini",
6072
- "initial"
6073
- ]
6074
- }]);
6075
- await analyzeController.bindWatcher(buildResult).runInitialUpdate();
6076
- }
6077
- continue;
6078
- }
6079
- if (backend.descriptor.id !== "web") continue;
6080
- const webServerStartedAt = Date.now();
6081
- try {
6082
- webServer = await backend.driver.dev(ctx, options);
6083
- logger_default.success(`Web 开发服务启动完成,耗时:${formatDuration(Date.now() - webServerStartedAt)}`);
6084
- analyzeController.emitRuntimeEvents([{
6085
- kind: "system",
6086
- level: "success",
6087
- title: "web dev server started",
6088
- detail: "Web 开发服务器已启动,可与小程序调试 UI 并行工作。",
6089
- durationMs: Date.now() - webServerStartedAt,
6090
- tags: ["dev", "web"]
6091
- }]);
6092
- } catch (error) {
6093
- analyzeController.emitRuntimeEvents([{
6094
- kind: "diagnostic",
6095
- level: "error",
6096
- title: "web dev server failed",
6097
- detail: error instanceof Error ? error.message : String(error),
6098
- durationMs: Date.now() - webServerStartedAt,
6099
- tags: ["dev", "web"]
6100
- }]);
6101
- logger_default.error(error);
6102
- throw error;
6103
- }
6104
- }
6105
- if (miniBackend) {
6106
- logBuildAppFinish(configService, webServer, {
6107
- skipWeb: !webBackend,
6108
- uiUrls: analyzeHandle?.urls
6109
- });
6110
- devHotkeysSession?.restore();
6111
- } else if (webBackend) logBuildAppFinish(configService, webServer, { skipMini: true });
6112
- if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
6113
- analyzeController.emitRuntimeEvents([{
6114
- kind: "command",
6115
- level: "info",
6116
- title: "opening ide",
6117
- detail: "开发服务已就绪,准备打开 IDE 项目。",
6118
- tags: ["ide", "open"]
6119
- }]);
6120
- devHotkeysSession?.suspend();
6121
- try {
6122
- await miniProgramDevActions.openIde({
6123
- forceOpen: true,
6124
- forceReopen: false,
6125
- openStrategy: options.ideOpenStrategy ?? "cli"
6126
- });
6127
- } finally {
6128
- devHotkeysSession?.restore();
6129
- }
6130
- }
6131
- if (analyzeHandle) await analyzeController.waitForExit();
6132
- else if (targets.has("dev")) await waitForServeShutdownSignal();
6133
- } finally {
6134
- devHotkeysSession?.close();
6135
- await closeActiveForwardConsole();
6136
- for (const backend of [...targets.select("dev")].reverse()) await backend.driver.close(ctx);
5941
+ const started = await shutdown.run("startup", () => startServeCommand(root, options, shutdown));
5942
+ if (started?.waitForExit) await Promise.race([shutdown.signal, started.waitForExit()]);
5943
+ else if (started?.watch) await shutdown.signal;
5944
+ } catch (error) {
5945
+ failure = error;
6137
5946
  }
5947
+ await shutdown.close(failure);
6138
5948
  });
6139
5949
  }
6140
5950
  //#endregion
6141
5951
  //#region src/cli/commands/upload.ts
6142
- async function buildUploadTarget(cwd, platform, options, action) {
5952
+ async function buildUploadTarget(cwd, platform, options, action, item) {
6143
5953
  const targets = resolveRuntimeTargets({ platform });
6144
5954
  const ctx = await createCompilerContext({
6145
5955
  cwd,
@@ -6153,6 +5963,11 @@ async function buildUploadTarget(cwd, platform, options, action) {
6153
5963
  });
6154
5964
  try {
6155
5965
  const target = await createUploadTarget(ctx.configService, options, action);
5966
+ if (item) {
5967
+ item.platform = target.platform;
5968
+ item.requestedVersion = target.context.version;
5969
+ item.stage = "build";
5970
+ }
6156
5971
  logger_default.info(`[${action}:${target.platform}] 构建${target.context.version ? ` ${target.context.version}` : ""}`);
6157
5972
  await ctx.buildService.build({});
6158
5973
  return target;
@@ -6166,16 +5981,58 @@ async function runUploadCommand(root, options, action = "upload") {
6166
5981
  const platforms = resolveUploadPlatforms(options.platform ?? options.p);
6167
5982
  setCommandNodeEnv("production");
6168
5983
  const cwd = path$1.resolve(root ?? process.cwd());
6169
- options = await prepareAutoUploadMetadata(cwd, options, action);
6170
- for (const platform of platforms) await executeUploadTarget(await buildUploadTarget(cwd, platform, options, action), options, action);
5984
+ if (action === "preview") {
5985
+ options = await prepareAutoUploadMetadata(cwd, options, action);
5986
+ for (const platform of platforms) await executeUploadTarget(await buildUploadTarget(cwd, platform, options, action), options, action);
5987
+ return;
5988
+ }
5989
+ const report = {
5990
+ schemaVersion: 1,
5991
+ action: "upload",
5992
+ status: "success",
5993
+ results: platforms.map((platform) => ({
5994
+ platform: platform ?? null,
5995
+ stage: "prepare",
5996
+ status: "not-run"
5997
+ }))
5998
+ };
5999
+ let active = report.results[0];
6000
+ try {
6001
+ const timeoutMs = resolveUploadTimeout(options.timeout);
6002
+ options = await prepareAutoUploadMetadata(cwd, options, action);
6003
+ for (const item of report.results) item.requestedVersion = options.uv?.trim();
6004
+ for (const [index, platform] of platforms.entries()) {
6005
+ active = report.results[index];
6006
+ const target = await buildUploadTarget(cwd, platform, options, action, active);
6007
+ active.stage = "validate";
6008
+ const result = await executeUploadTarget(target, options, action, {
6009
+ timeoutMs,
6010
+ onUpload: () => {
6011
+ active.stage = "upload";
6012
+ }
6013
+ });
6014
+ active.status = options.dryRun ? "dry-run" : "success";
6015
+ if (!options.dryRun) active.result = result;
6016
+ }
6017
+ return report;
6018
+ } catch (error) {
6019
+ report.status = "failed";
6020
+ report.error = error instanceof Error ? error.message : String(error);
6021
+ active.status = "failed";
6022
+ active.error = report.error;
6023
+ if (error instanceof UploadExecutionError) {
6024
+ active.remoteOutcome = error.remoteOutcome;
6025
+ if (error.remoteOutcome === "unknown" && error.reason !== "failed") active.status = "unknown";
6026
+ }
6027
+ throw new UploadCommandError(report);
6028
+ }
6171
6029
  }
6172
6030
  function registerUploadCommand(cli) {
6173
- cli.command("upload [root]", "build and upload mini programs (does not submit for review or publish)").option("-p, --platform <platform>", "[string] weapp | alipay | tt | xhs | jd | swan; comma-separated targets or all").option("--project-config <path>", "[string] project config path").option("--uv <version>", "[string] upload version (default: weapp.upload.version or package.json version)").option("--desc <text>", "[string] upload description (default: weapp.upload.desc or project name and version)").option("--bump <release>", "[string] increment local package version once (patch | minor | major; dry-run does not write)").option("--git-desc", "[boolean] use the latest Git commit subject as upload description").option("--dry-run", "[boolean] build without validating upload credentials or uploading").example("wv upload -p weapp").example("wv upload -p xhs,tt --bump patch --git-desc --dry-run").example("wv upload --project ./dist --version 1.2.3 --desc \"release\" (deprecated IDE syntax; use wv ide upload to retain IDE behavior)").action(async (root, options) => {
6174
- await runUploadCommand(root, {
6031
+ cli.command("upload [root]", "build and upload mini programs (does not submit for review or publish)").option("-p, --platform <platform>", "[string] weapp | alipay | tt | xhs | jd | swan; comma-separated targets or all").option("--project-config <path>", "[string] project config path").option("--uv <version>", "[string] upload version (default: weapp.upload.version or package.json version)").option("--desc <text>", "[string] upload description (default: weapp.upload.desc or project name and version)").option("--bump <release>", "[string] increment local package version once (patch | minor | major; dry-run does not write)").option("--git-desc", "[boolean] use the latest Git commit subject as upload description").option("--dry-run", "[boolean] build without validating upload credentials or uploading").option("--json", "[boolean] write one upload report to stdout; send logs to stderr").option("--timeout <seconds>", "[number] local timeout per platform SDK invocation, excluding build").example("wv upload -p weapp").example("wv upload -p xhs,tt --bump patch --git-desc --dry-run").example("wv upload --project ./dist --version 1.2.3 --desc \"release\" (deprecated IDE syntax; use wv ide upload to retain IDE behavior)").action(async (root, options) => {
6032
+ return runUploadCommand(root, {
6175
6033
  ...options,
6176
6034
  ...readUploadMetadata(cli)
6177
6035
  });
6178
- scheduleCompletedProductionBuildExit({}, void 0);
6179
6036
  });
6180
6037
  }
6181
6038
  function registerPreviewCommand(cli) {
@@ -6241,7 +6098,8 @@ const WEAPP_VITE_NATIVE_COMMANDS = /* @__PURE__ */ new Set([
6241
6098
  "build-npm",
6242
6099
  "generate",
6243
6100
  "g",
6244
- "mcp"
6101
+ "mcp",
6102
+ "accept"
6245
6103
  ]);
6246
6104
  const LEGACY_UPLOAD_OPTIONS = {
6247
6105
  "--version": true,
@@ -6257,7 +6115,9 @@ const SDK_UPLOAD_OPTIONS = {
6257
6115
  "--uv": true,
6258
6116
  "--bump": true,
6259
6117
  "--git-desc": true,
6260
- "--dry-run": true
6118
+ "--dry-run": true,
6119
+ "--json": true,
6120
+ "--timeout": true
6261
6121
  };
6262
6122
  const UPLOAD_VALUE_OPTIONS = {
6263
6123
  ...LEGACY_UPLOAD_OPTIONS,
@@ -6267,6 +6127,7 @@ const UPLOAD_VALUE_OPTIONS = {
6267
6127
  "--platform": true,
6268
6128
  "--uv": true,
6269
6129
  "--bump": true,
6130
+ "--timeout": true,
6270
6131
  "--project-config": true,
6271
6132
  "--port": true,
6272
6133
  "--lang": true,
@@ -6409,15 +6270,10 @@ function handlePrepareLifecycleError(args, error) {
6409
6270
  //#endregion
6410
6271
  //#region src/cli.ts
6411
6272
  const cli = cac("weapp-vite");
6412
- try {
6413
- checkRuntime({
6414
- bun: "0.0.0",
6415
- deno: "0.0.0",
6416
- node: "^20.19.0 || >=22.12.0"
6417
- });
6418
- } catch {}
6419
6273
  cli.option("-c, --config <file>", `[string] use specified config file`).option("--base <path>", `[string] public base path (default: /)`, { type: [convertBase] }).option("-l, --logLevel <level>", `[string] info | warn | error | silent`).option("--clearScreen", `[boolean] allow/disable clear screen when logging`).option("-d, --debug [feat]", `[string | boolean] show debug logs`).option("-f, --filter <filter>", `[string] filter debug logs`).option("-m, --mode <mode>", `[string] set env mode`);
6274
+ registerAcceptCommand(cli);
6420
6275
  registerIdeCommand(cli);
6276
+ registerDoctorCommand(cli);
6421
6277
  registerAlipayCommand(cli);
6422
6278
  registerBuildCommand(cli);
6423
6279
  registerUploadCommand(cli);
@@ -6440,7 +6296,9 @@ const skipManagedTsconfigBootstrapCommands = /* @__PURE__ */ new Set([
6440
6296
  "init",
6441
6297
  "mcp",
6442
6298
  "npm",
6443
- "alipay"
6299
+ "alipay",
6300
+ "doctor",
6301
+ "accept"
6444
6302
  ]);
6445
6303
  function resolveManagedTsconfigBootstrapRoot(args) {
6446
6304
  const [firstArg, secondArg] = args;
@@ -6472,11 +6330,19 @@ function resolveManagedTsconfigBootstrapRoot(args) {
6472
6330
  try {
6473
6331
  const args = process.argv.slice(2);
6474
6332
  if (!await tryRunIdeCommand(args)) {
6475
- const managedTsconfigBootstrapRoot = resolveManagedTsconfigBootstrapRoot(args);
6476
- if (managedTsconfigBootstrapRoot) await syncManagedTsconfigBootstrapFiles(managedTsconfigBootstrapRoot);
6477
6333
  cli.parse(process.argv, { run: false });
6478
- if (cli.matchedCommand?.name === "" && cli.matchedCommandName === void 0) await maybeAutoStartMcpServer(args, cli.options);
6479
- await cli.runMatchedCommand();
6334
+ const runCommand = async () => {
6335
+ const uiHost = Array.isArray(cli.options.uiHost) ? cli.options.uiHost.at(-1) : cli.options.uiHost;
6336
+ parseDashboardUiHost(uiHost);
6337
+ const managedTsconfigBootstrapRoot = resolveManagedTsconfigBootstrapRoot(args);
6338
+ if (managedTsconfigBootstrapRoot) await syncManagedTsconfigBootstrapFiles(managedTsconfigBootstrapRoot);
6339
+ if (cli.matchedCommand?.name === "" && cli.matchedCommandName === void 0) await maybeAutoStartMcpServer(args, cli.options);
6340
+ return cli.runMatchedCommand();
6341
+ };
6342
+ if (cli.matchedCommand?.name === "upload") {
6343
+ await outputUploadReport((Array.isArray(cli.options.json) ? cli.options.json.at(-1) : cli.options.json) === true, runCommand);
6344
+ scheduleCompletedProductionBuildExit({}, void 0);
6345
+ } else await runCommand();
6480
6346
  }
6481
6347
  } catch (error) {
6482
6348
  if (!handlePrepareLifecycleError(process.argv.slice(2), error)) {