weapp-vite 7.3.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 (91) hide show
  1. package/README.md +154 -6
  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 +1918 -2089
  14. package/dist/compatibility.mjs +1 -2
  15. package/dist/{config-1J4rToMR.d.mts → config-V0L-IAJf.d.mts} +419 -39
  16. package/dist/config.d.mts +2 -2
  17. package/dist/config.mjs +1 -1
  18. package/dist/constants-DEZdcsMn.mjs +85 -0
  19. package/dist/{createContext-cqr9Y4EV.mjs → createCompilerContextInstance-DJH_P_nZ.mjs} +17602 -15886
  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 +9 -0
  24. package/dist/dashboard/vite.mjs +44 -0
  25. package/dist/dashboard-DvqF3Key.mjs +1842 -0
  26. package/dist/dashboard.d.mts +3 -0
  27. package/dist/dashboard.mjs +1 -0
  28. package/dist/dashboardViteBridge-Dy3nAD4m.mjs +299 -0
  29. package/dist/dev-fR5GiJRO.mjs +63 -0
  30. package/dist/devProvider-BGBkw28J.mjs +1050 -0
  31. package/dist/docs/README.md +154 -6
  32. package/dist/docs/ai-workflows.md +4 -0
  33. package/dist/docs/doctor.md +38 -0
  34. package/dist/docs/getting-started.md +52 -1
  35. package/dist/docs/index.md +6 -2
  36. package/dist/docs/mcp.md +94 -1
  37. package/dist/docs/testing.md +29 -2
  38. package/dist/docs/troubleshooting.md +8 -0
  39. package/dist/docs/uni-app-component-libraries.md +8 -7
  40. package/dist/docs/upload.md +226 -0
  41. package/dist/docs/vite-plugin.md +171 -0
  42. package/dist/docs/vue-sfc.md +19 -0
  43. package/dist/docs/weapp-config.md +374 -2
  44. package/dist/docs/wevu-authoring.md +10 -0
  45. package/dist/doctor-worker.d.mts +1 -0
  46. package/dist/doctor-worker.mjs +59 -0
  47. package/dist/doctor.d.mts +144 -0
  48. package/dist/doctor.mjs +848 -0
  49. package/dist/eslint.mjs +1 -2
  50. package/dist/file-DRb4eWqw.mjs +1 -0
  51. package/dist/{file-CKB0Tn8Z.mjs → file-sC3jD7Fh.mjs} +16 -93
  52. package/dist/files-BqxFIPRp.mjs +45 -0
  53. package/dist/getInstance-DiGSzBNI.mjs +28 -0
  54. package/dist/getInstance-SiE6WZRh.mjs +1 -0
  55. package/dist/index-C9HjUX5c.d.mts +741 -0
  56. package/dist/index.d.mts +10 -8
  57. package/dist/index.mjs +3 -2
  58. package/dist/json.d.mts +1 -1
  59. package/dist/json.mjs +1 -2
  60. package/dist/lifecycle-BROTlwSL.mjs +320 -0
  61. package/dist/loadConfig-C3BQhz7F.mjs +59 -0
  62. package/dist/loadConfig-VGtBF4qm.mjs +1 -0
  63. package/dist/logger-DGn5iEVN.mjs +1 -0
  64. package/dist/{mcp-BG6TliEg.mjs → mcp-Cj8Maqo6.mjs} +16 -7
  65. package/dist/mcp-D3Yh24Cj.mjs +529 -0
  66. package/dist/mcp.d.mts +6 -3
  67. package/dist/mcp.mjs +1 -2
  68. package/dist/package-7swhyLmf.mjs +91 -0
  69. package/dist/path-doV773b7.mjs +103 -0
  70. package/dist/platform-CnkySBKz.mjs +175 -0
  71. package/dist/processCleanup-B6KA30Zc.mjs +77 -0
  72. package/dist/protocol-vg8HGJ4n.mjs +138 -0
  73. package/dist/{resolvers-icNxkpcy.mjs → resolvers-Dod-euqX.mjs} +4 -1
  74. package/dist/resolvers.mjs +1 -2
  75. package/dist/runtime.mjs +1 -2
  76. package/dist/session-PskhD-nZ.mjs +390 -0
  77. package/dist/test.d.mts +1 -0
  78. package/dist/test.mjs +201 -44
  79. package/dist/types.d.mts +2 -2
  80. package/dist/upload-D4msblBf.mjs +562 -0
  81. package/dist/upload-worker.d.mts +1 -0
  82. package/dist/upload-worker.mjs +161 -0
  83. package/dist/utils-CJSCgEj6.mjs +1094 -0
  84. package/dist/vite.d.mts +9 -0
  85. package/dist/vite.mjs +270 -0
  86. package/dist/volar.d.mts +2 -2
  87. package/dist/web-apis.mjs +1 -2
  88. package/package.json +85 -37
  89. package/dist/file-CvgkbGIx.mjs +0 -2
  90. package/dist/getInstance-CZ1s63zd.mjs +0 -2
  91. package/dist/logger-mt4mSTqV.mjs +0 -2
package/dist/cli.mjs CHANGED
@@ -1,35 +1,47 @@
1
- import { A as getDefaultIdeProjectRoot, C as collectPreloadPages, D as loadViteConfigFile, E as suggestPreloadRules, M as isPathInside, 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, j as shouldPassPlatformArgToIdeOpen, k as parseCommentJson, m as syncManagedTsconfigBootstrapFiles, p as syncProjectSupportFiles, t as createCompilerContext, v as resolveHmrProfileJsonPath, w as findPreloadRuleKey, x as checkRuntime } from "./createContext-cqr9Y4EV.mjs";
2
- import { f as VERSION, i as findJsEntry, o as findTemplateEntry, s as findVueEntry } from "./file-CKB0Tn8Z.mjs";
3
- import { r as logger_default, t as colors } from "./logger-mt4mSTqV.mjs";
4
- import { c as startWeappViteMcpServer, l as detectAiDevelopmentEnvironment, s as resolveWeappMcpConfig } from "./mcp-BG6TliEg.mjs";
5
- import { createRequire } from "node:module";
6
- import fs from "node:fs";
1
+ import { a as resolveWeappViteHostMeta } from "./pluginHost--CaeyWpA.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";
7
20
  import path, { posix } from "pathe";
21
+ import fs, { readFileSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
8
23
  import path$1 from "node:path";
9
24
  import { defu } from "@weapp-core/shared";
10
25
  import { fs as fs$1 } from "@weapp-core/shared/fs";
11
- import process from "node:process";
12
26
  import { parse } from "vue/compiler-sfc";
13
- import fs$2, { mkdir, writeFile } from "node:fs/promises";
14
- import { build, createServer } from "vite";
15
- import { createHash } from "node:crypto";
16
- import { promisify } from "node:util";
17
- import os from "node:os";
18
- import { execFile, 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";
19
31
  import { Buffer } from "node:buffer";
32
+ import { fork, spawn } from "node:child_process";
20
33
  import { cac } from "cac";
21
- 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";
22
35
  import { brotliCompressSync, gzipSync } from "node:zlib";
23
36
  import { buildOtpAuthUrl, refreshTempAuthCode } from "devframe/node/auth";
24
37
  import { resolveCommand } from "package-manager-detector/commands";
25
- import { defineDevframe, defineRpcFunction } from "devframe";
26
- import { Server } from "node:http";
27
- import { initDevframe } from "devframe/initiate";
38
+ import semverInc from "semver/functions/inc.js";
39
+ import { x } from "tinyexec";
40
+ import { parse as parse$2 } from "dotenv";
41
+ import { expand } from "dotenv-expand";
28
42
  import { generateJs, generateJson, generateWxml, generateWxss } from "@weapp-core/schematics";
29
43
  import { determineAgent } from "@vercel/detect-agent";
30
44
  import { initConfig } from "@weapp-core/init";
31
- import { createInterface } from "node:readline/promises";
32
- import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from "node:timers";
33
45
  import net from "node:net";
34
46
  //#region src/cli/runtime.ts
35
47
  function logRuntimeTarget(targets, options = {}) {
@@ -55,6 +67,9 @@ function resolveRuntimeTargets(options) {
55
67
  rawPlatform
56
68
  };
57
69
  }
70
+ function resolveConfiguredRuntimeTargets(targets, platform) {
71
+ return targets.rawPlatform === void 0 && platform === "web" ? resolveRuntimeTargets({ platform: "web" }) : targets;
72
+ }
58
73
  function createInlineConfig(execution, options = {}) {
59
74
  const configs = execution.entries.map((entry) => entry.driver.createInlineConfig({
60
75
  execution,
@@ -198,33 +213,10 @@ async function executeWechatIdeCliCommand(argv, options = {}) {
198
213
  }
199
214
  //#endregion
200
215
  //#region src/cli/openIde/close.ts
201
- const execFileAsync = promisify(execFile);
202
- async function closeIdeByAppleScript() {
203
- if (process.platform !== "darwin") return false;
204
- const appName = process.env.WEAPP_DEVTOOLS_APP_NAME || "wechatwebdevtools";
205
- try {
206
- await execFileAsync("osascript", ["-e", `tell application "${appName}" to quit`]);
207
- return true;
208
- } catch {
209
- return false;
210
- }
211
- }
212
- async function closeIdeByProcessKill(cliPath) {
213
- if (!cliPath) return false;
214
- const appContentsRoot = cliPath.includes(".app/") ? cliPath.slice(0, cliPath.indexOf(".app/") + 4) : path.dirname(path.dirname(cliPath));
215
- try {
216
- await execFileAsync("pkill", ["-f", appContentsRoot]);
217
- return true;
218
- } catch {
219
- return false;
220
- }
221
- }
222
216
  /**
223
- * @description 关闭微信开发者工具,并在 CLI 不可用时回退到系统级关闭。
217
+ * @description 执行用户显式请求的 CLI close;失败时保留宿主,不升级为系统级退出。
224
218
  */
225
219
  async function closeIde$1() {
226
- const config = await getConfig();
227
- const cliPath = config.cliPath?.trim() ? config.cliPath : null;
228
220
  try {
229
221
  await closeWechatIdeProject();
230
222
  return true;
@@ -240,17 +232,9 @@ async function closeIde$1() {
240
232
  logger_default.error(retryError);
241
233
  }
242
234
  else {
243
- logger_default.warn("微信开发者工具 CLI close 执行失败,尝试回退为系统级关闭。");
235
+ logger_default.warn("微信开发者工具 CLI close 执行失败,已保留现有宿主;请在开发者工具中手动关闭目标项目。");
244
236
  logger_default.error(error);
245
237
  }
246
- if (await closeIdeByAppleScript()) {
247
- logger_default.info("已回退为系统级关闭微信开发者工具。");
248
- return true;
249
- }
250
- if (await closeIdeByProcessKill(cliPath)) {
251
- logger_default.info("已回退为进程级关闭微信开发者工具。");
252
- return true;
253
- }
254
238
  return false;
255
239
  }
256
240
  }
@@ -299,7 +283,7 @@ function logWechatIdeServicePortDisabledHint(projectPath) {
299
283
  const OPENED_PROJECT_HEALTH_CHECK_TIMEOUT = 3e3;
300
284
  const OPEN_AUTOMATOR_TIMEOUT = 12e4;
301
285
  function formatReuseOpenedWechatIdePrompt() {
302
- 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(" / ")} 重新连接目标项目(保留现有窗口)。`;
303
287
  }
304
288
  function disconnectMiniProgram(miniProgram) {
305
289
  miniProgram.disconnect();
@@ -354,7 +338,7 @@ async function connectOpenedProject(projectPath) {
354
338
  /**
355
339
  * @description 若当前项目已在微信开发者工具中打开且自动化可连通,则直接复用现有会话,避免重复拉起 IDE。
356
340
  */
357
- async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
341
+ async function tryReuseOpenedWechatIde(projectPath, options = {}) {
358
342
  const connection = await connectOpenedProject(projectPath);
359
343
  if (connection.status === "unhealthy") return {
360
344
  reopened: false,
@@ -376,8 +360,7 @@ async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
376
360
  reopened: false,
377
361
  reused: true
378
362
  };
379
- logger_default.info(colors.bold(colors.green("正在关闭当前已打开项目,并重新拉起微信开发者工具...")));
380
- if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
363
+ logger_default.info(colors.bold(colors.green("正在重新连接目标项目,保留现有微信开发者工具窗口...")));
381
364
  await openWechatIdeByAutomator(projectPath, {
382
365
  preserveProjectRoot: options.preserveProjectRoot,
383
366
  trustProject: options.trustProject
@@ -388,16 +371,15 @@ async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
388
371
  };
389
372
  }
390
373
  /**
391
- * @description 对已打开的目标项目执行强制重开,以刷新最新构建产物。
374
+ * @description 重新连接目标项目以刷新构建产物;连接失败不授予关闭共享宿主的权限。
392
375
  */
393
- async function reopenOpenedWechatIde(projectPath, closeIde, options = {}) {
376
+ async function reopenOpenedWechatIde(projectPath, options = {}) {
394
377
  const connection = await connectOpenedProject(projectPath);
395
378
  if (connection.status === "connected") {
396
379
  disconnectMiniProgram(connection.miniProgram);
397
- logger_default.info("目标项目已在微信开发者工具中打开,当前命令将主动重开以刷新最新构建产物。");
398
- } else if (connection.status === "unhealthy") logger_default.info("目标项目的 automator 会话未通过健康检查,当前命令将关闭窗口并重新拉起。");
399
- else logger_default.info("未检测到可复用的 automator 会话,当前命令将关闭现有窗口并重新拉起目标项目。");
400
- 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 会话,将尝试打开目标项目,保留其他窗口。");
401
383
  await openWechatIdeByAutomator(projectPath, options);
402
384
  return true;
403
385
  }
@@ -408,11 +390,7 @@ function shouldLogAutomatorFallbackError() {
408
390
  return flag === "1" || flag === "true";
409
391
  }
410
392
  const PREPARE_AUTOMATOR_SESSION_TIMEOUT = 8e3;
411
- const AUTOMATOR_RECOVERY_QUIT_SETTLE_MS = 5e3;
412
393
  const RESET_FILEUTILS_ENV = "WEAPP_VITE_RESET_IDE_FILEUTILS";
413
- function sleep$1(ms) {
414
- return new Promise((resolve) => setTimeout(resolve, ms));
415
- }
416
394
  function readProjectConfigObject(projectPath) {
417
395
  try {
418
396
  const raw = fs.readFileSync(path.join(projectPath, "project.config.json"), "utf8");
@@ -441,7 +419,10 @@ function shouldResetWechatIdeFileUtils() {
441
419
  async function runWechatIdeOpenWithRetry(argv) {
442
420
  await executeWechatIdeCliCommand(argv, {
443
421
  cancelLevel: "warn",
444
- 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
+ },
445
426
  onRetry: () => {
446
427
  logger_default.info(colors.bold(colors.green("正在重试连接微信开发者工具...")));
447
428
  },
@@ -449,10 +430,11 @@ async function runWechatIdeOpenWithRetry(argv) {
449
430
  });
450
431
  }
451
432
  /**
452
- * @description 根据 mpDistRoot 推导 IDE 项目目录(目录内应包含 project/mini 配置)
433
+ * @description 根据代码输出目录推导 IDE 项目目录;内联项目配置与 app.json 同目录。
453
434
  */
454
- function resolveIdeProjectPath(mpDistRoot) {
435
+ function resolveIdeProjectPath(mpDistRoot, generatedProjectConfig = false) {
455
436
  if (!mpDistRoot || !mpDistRoot.trim()) return;
437
+ if (generatedProjectConfig) return mpDistRoot;
456
438
  const parent = path.dirname(mpDistRoot);
457
439
  if (!parent || parent === "." || parent === "/") return;
458
440
  return parent;
@@ -460,31 +442,22 @@ function resolveIdeProjectPath(mpDistRoot) {
460
442
  /**
461
443
  * @description 结合 mpDistRoot 与配置根目录解析最终 IDE 项目目录。
462
444
  */
463
- function resolveIdeProjectRoot(mpDistRoot, cwd) {
464
- return resolveIdeProjectPath(mpDistRoot) ?? cwd;
445
+ function resolveIdeProjectRoot(mpDistRoot, cwd, generatedProjectConfig = false) {
446
+ const projectPath = resolveIdeProjectPath(mpDistRoot, generatedProjectConfig);
447
+ return generatedProjectConfig && projectPath && cwd ? path.resolve(cwd, projectPath) : projectPath ?? cwd;
465
448
  }
466
449
  async function closeIde() {
467
450
  return await closeIde$1();
468
451
  }
469
- async function restartWechatIdeForAutomatorRecovery() {
470
- try {
471
- await quitWechatIde();
472
- await sleep$1(AUTOMATOR_RECOVERY_QUIT_SETTLE_MS);
473
- return true;
474
- } catch {
475
- return await closeIde();
476
- }
477
- }
478
452
  async function tryOpenWechatIdeByAutomator(projectPath, options) {
479
453
  const preserveProjectRoot = !shouldUseAutomatorProjectWrapper(projectPath);
480
454
  if (options.reuseOpenedProject === false) {
481
- if (await reopenOpenedWechatIde(projectPath, closeIde, {
455
+ if (await reopenOpenedWechatIde(projectPath, {
482
456
  preserveProjectRoot,
483
- restartIde: restartWechatIdeForAutomatorRecovery,
484
457
  trustProject: options.trustProject
485
458
  })) return "reopened";
486
459
  }
487
- const reuseResult = await tryReuseOpenedWechatIde(projectPath, closeIde, {
460
+ const reuseResult = await tryReuseOpenedWechatIde(projectPath, {
488
461
  preserveProjectRoot,
489
462
  promptReopen: options.reuseOpenedProject !== true,
490
463
  trustProject: options.trustProject
@@ -649,12 +622,11 @@ async function recoverOpenedWechatIdeProject(platform, projectPath, servicePortE
649
622
  logger_default.warn("已跳过微信开发者工具自动恢复;请按上方提示手动关闭并重新打开目标项目。");
650
623
  return failedResult;
651
624
  }
652
- logger_default.info(`检测到微信开发者工具打开后状态不稳定(${formatWechatIdeOpenHealthReason(failedResult)}),正在自动关闭并重新打开目标项目...`);
653
- if (!await closeIde()) logger_default.warn("自动恢复时关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
625
+ logger_default.info(`检测到微信开发者工具打开后状态不稳定(${formatWechatIdeOpenHealthReason(failedResult)}),正在重试打开目标项目(保留现有窗口)...`);
654
626
  await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, options));
655
627
  const recoveredResult = await verifyOpenedWechatIdeProject(projectPath, servicePortEnabled, options);
656
628
  if (recoveredResult.ok) logger_default.info("微信开发者工具已完成自动恢复。");
657
- 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` 跳过自动重试,并按提示手动处理。");
658
630
  return recoveredResult;
659
631
  }
660
632
  async function verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, servicePortEnabled, options) {
@@ -691,6 +663,7 @@ async function openIde(platform, projectPath, options = {}) {
691
663
  return;
692
664
  }
693
665
  } catch (error) {
666
+ if (process.env.WEAPP_IDE_MANAGED_PROJECT_JOURNAL?.trim()) throw error;
694
667
  if (isAutomatorLoginError(error)) {
695
668
  logger_default.error("检测到微信开发者工具登录状态失效,请先登录后重试。");
696
669
  logger_default.warn(formatAutomatorLoginError(error));
@@ -698,9 +671,6 @@ async function openIde(platform, projectPath, options = {}) {
698
671
  logger_default.warn(`通过 automator 启动微信开发者工具失败,回退到普通 open 流程:${formatWechatIdeAutomatorFailure(error)}`);
699
672
  if (shouldLogAutomatorFallbackError()) logger_default.error(error);
700
673
  }
701
- else if (platform === "weapp" && projectPath && normalizedOptions.reuseOpenedProject === false) {
702
- if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试打开目标项目。");
703
- }
704
674
  await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, normalizedOptions));
705
675
  if (platform === "weapp" && projectPath && normalizedOptions.prepareAutomatorSession) await prepareWechatIdeAutomatorSession(projectPath, normalizedOptions);
706
676
  if (platform === "weapp" && projectPath && !normalizedOptions.skipPostOpenHealthCheck) await verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, bootstrapResult?.servicePortEnabled, normalizedOptions);
@@ -722,7 +692,7 @@ async function resolveIdeCommandContext(options) {
722
692
  cliPlatform: options.cliPlatform
723
693
  });
724
694
  platform ??= ctx.configService.platform;
725
- if (!projectPath) projectPath = resolveIdeProjectRoot(ctx.configService.mpDistRoot, ctx.configService.cwd);
695
+ if (!projectPath) projectPath = resolveIdeProjectRoot(ctx.configService.mpDistRoot, ctx.configService.cwd, ctx.configService.multiPlatform.projectConfigs !== void 0);
726
696
  return {
727
697
  cwd: ctx.configService.cwd,
728
698
  platform,
@@ -742,35 +712,6 @@ async function resolveIdeCommandContext(options) {
742
712
  };
743
713
  }
744
714
  //#endregion
745
- //#region src/cli/options.ts
746
- function filterDuplicateOptions(options) {
747
- for (const [key, value] of Object.entries(options)) if (Array.isArray(value)) options[key] = value[value.length - 1];
748
- }
749
- function resolveConfigFile(options) {
750
- if (typeof options.config === "string") return options.config;
751
- if (typeof options.c === "string") return options.c;
752
- }
753
- function convertBase(value) {
754
- if (value === 0) return "";
755
- return value;
756
- }
757
- function coerceBooleanOption(value) {
758
- if (value === void 0) return;
759
- if (typeof value === "boolean") return value;
760
- if (typeof value === "string") {
761
- const normalized = value.trim().toLowerCase();
762
- if (normalized === "") return true;
763
- if (normalized === "false" || normalized === "0" || normalized === "off" || normalized === "no") return false;
764
- if (normalized === "true" || normalized === "1" || normalized === "on" || normalized === "yes") return true;
765
- return true;
766
- }
767
- if (typeof value === "number") return value !== 0;
768
- return Boolean(value);
769
- }
770
- function isUiEnabled(options) {
771
- return Boolean(options.ui || options.analyze);
772
- }
773
- //#endregion
774
715
  //#region src/cli/commands/alipayExecute.ts
775
716
  function createSpawnOptions() {
776
717
  return {
@@ -856,6 +797,154 @@ function registerAlipayCommand(cli) {
856
797
  });
857
798
  }
858
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
859
948
  //#region src/analyze/hmr.ts
860
949
  function createMetricSummary(values) {
861
950
  if (!values.length) return { count: 0 };
@@ -894,26 +983,19 @@ function isFiniteNumber$1(value) {
894
983
  * @description 聚合 HMR JSONL profile,为命令行与后续仪表盘复用。
895
984
  */
896
985
  async function analyzeHmrProfile(options) {
897
- const lines = (await fs$1.readFile(options.profilePath, "utf8")).split(/\r?\n/);
898
- const samples = [];
899
- let skippedLineCount = 0;
900
- for (const line of lines) {
901
- const trimmed = line.trim();
902
- if (!trimmed) continue;
903
- try {
904
- const parsed = JSON.parse(trimmed);
905
- if (!isFiniteNumber$1(parsed.totalMs)) {
906
- skippedLineCount += 1;
907
- continue;
908
- }
909
- samples.push(parsed);
910
- } catch {
911
- skippedLineCount += 1;
912
- }
913
- }
986
+ const { samples, coverage: inputCoverage, skippedLineCount } = readHmrProfileLines(await fs$1.readFile(options.profilePath, "utf8"));
914
987
  const eventCounts = /* @__PURE__ */ new Map();
915
988
  const dirtyReasonCounts = /* @__PURE__ */ new Map();
916
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 = [];
917
999
  const totalValues = [];
918
1000
  const buildCoreValues = [];
919
1001
  const buildStartValues = [];
@@ -963,6 +1045,15 @@ async function analyzeHmrProfile(options) {
963
1045
  const resolveCountValues = [];
964
1046
  const skippedLoadedCountValues = [];
965
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);
966
1057
  totalValues.push(sample.totalMs);
967
1058
  if (sample.event) eventCounts.set(sample.event, (eventCounts.get(sample.event) ?? 0) + 1);
968
1059
  if (isFiniteNumber$1(sample.buildCoreMs)) buildCoreValues.push(sample.buildCoreMs);
@@ -1029,9 +1120,24 @@ async function analyzeHmrProfile(options) {
1029
1120
  profilePath: options.profilePath,
1030
1121
  sampleCount: samples.length,
1031
1122
  skippedLineCount,
1123
+ inputCoverage,
1124
+ timelines: samples.map((sample) => ({
1125
+ batchId: sample.batchId,
1126
+ completionBoundary: sample.completionBoundary,
1127
+ ...attributeHmrProfile(sample)
1128
+ })),
1032
1129
  firstTimestamp: orderedByTime[0]?.timestamp,
1033
1130
  lastTimestamp: orderedByTime[orderedByTime.length - 1]?.timestamp,
1034
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),
1035
1141
  totalMs: createMetricSummary(totalValues),
1036
1142
  buildCoreMs: createMetricSummary(buildCoreValues),
1037
1143
  buildStartMs: createMetricSummary(buildStartValues),
@@ -1092,7 +1198,7 @@ async function analyzeHmrProfile(options) {
1092
1198
  //#endregion
1093
1199
  //#region src/analyze/preload/budget.ts
1094
1200
  const PRELOAD_LIMIT_BYTES = 2097152;
1095
- function createPackageSizeMap$1(result) {
1201
+ function createPackageSizeMap(result) {
1096
1202
  return new Map((result?.packages ?? []).map((pkg) => [pkg.id, pkg.files.reduce((total, file) => total + (file.size ?? 0), 0)]));
1097
1203
  }
1098
1204
  function getConfiguredPackages(rule, appJson) {
@@ -1102,7 +1208,7 @@ function getConfiguredPackages(rule, appJson) {
1102
1208
  return packages.flatMap((item) => typeof item === "string" ? [resolvePreloadPackageIdentifier(item, appJson)] : []).filter((item) => Boolean(item));
1103
1209
  }
1104
1210
  function createPreloadBudgets(appJson, result, configuredRules, packageAnalysis) {
1105
- const packageSizes = createPackageSizeMap$1(packageAnalysis);
1211
+ const packageSizes = createPackageSizeMap(packageAnalysis);
1106
1212
  const bySourcePackage = /* @__PURE__ */ new Map();
1107
1213
  const ensureSourcePackage = (page) => {
1108
1214
  const sourcePackage = page.packageRoot ?? "__main__";
@@ -1431,140 +1537,100 @@ function analyzeComponentUsage(options) {
1431
1537
  });
1432
1538
  }
1433
1539
  //#endregion
1434
- //#region src/analyze/subpackages/metadata.ts
1435
- const defaultTotalBudgetBytes = 20971520;
1436
- const defaultPackageBudgetBytes = 2097152;
1437
- const defaultWarningRatio = .85;
1438
- const defaultHistoryDir = ".weapp-vite/analyze-history";
1439
- const defaultHistoryLimit = 20;
1440
- function resolveBudgetValue(value, fallback) {
1441
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1442
- }
1443
- function resolveHistoryLimit(value) {
1444
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : defaultHistoryLimit;
1445
- }
1446
- function resolveAnalyzeBudgets(configService) {
1447
- const budgets = configService.weappViteConfig.analyze?.budgets;
1448
- const legacyPackageBudget = configService.weappViteConfig.packageSizeWarningBytes;
1449
- const packageFallback = resolveBudgetValue(legacyPackageBudget, defaultPackageBudgetBytes);
1450
- return {
1451
- totalBytes: resolveBudgetValue(budgets?.totalBytes, defaultTotalBudgetBytes),
1452
- mainBytes: resolveBudgetValue(budgets?.mainBytes, packageFallback),
1453
- subPackageBytes: resolveBudgetValue(budgets?.subPackageBytes, packageFallback),
1454
- independentBytes: resolveBudgetValue(budgets?.independentBytes, packageFallback),
1455
- warningRatio: resolveBudgetValue(budgets?.warningRatio, defaultWarningRatio),
1456
- source: budgets ? "config" : "default"
1457
- };
1458
- }
1459
- function resolveAnalyzeHistoryMetadata(configService) {
1460
- const history = configService.weappViteConfig.analyze?.history;
1461
- if (history === false) return {
1462
- enabled: false,
1463
- dir: path.resolve(configService.cwd, defaultHistoryDir),
1464
- limit: defaultHistoryLimit
1465
- };
1466
- const historyConfig = typeof history === "object" && history ? history : {};
1467
- const rawDir = typeof historyConfig.dir === "string" && historyConfig.dir.trim() ? historyConfig.dir.trim() : defaultHistoryDir;
1468
- return {
1469
- enabled: historyConfig.enabled !== false,
1470
- dir: path.isAbsolute(rawDir) ? rawDir : path.resolve(configService.cwd, rawDir),
1471
- limit: resolveHistoryLimit(historyConfig.limit)
1472
- };
1473
- }
1474
- function createAnalyzeMetadata(configService, now = /* @__PURE__ */ new Date()) {
1475
- const history = resolveAnalyzeHistoryMetadata(configService);
1476
- const packageName = configService.packageJson?.name;
1477
- return {
1478
- projectName: typeof packageName === "string" && packageName.trim() ? packageName.trim() : path.basename(configService.cwd),
1479
- generatedAt: now.toISOString(),
1480
- budgets: resolveAnalyzeBudgets(configService),
1481
- history: {
1482
- ...history,
1483
- 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;
1484
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;
1485
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";
1486
1576
  }
1487
1577
  //#endregion
1488
- //#region src/analyze/subpackages/classifier.ts
1489
- const VIRTUAL_MODULE_INDICATOR = "\0";
1490
- const VIRTUAL_PREFIX = `${SHARED_CHUNK_VIRTUAL_PREFIX}/`;
1491
- function classifyModuleSourceKind(options) {
1492
- if (options.isNodeModule) return "node_modules";
1493
- if (options.inSrc) return "src";
1494
- if (options.inPlugin) return "plugin";
1495
- return "workspace";
1496
- }
1497
- function resolvePluginAssetAbsolute(normalizedFileName, pluginRoot) {
1498
- if (!pluginRoot) return;
1499
- if (posix.isAbsolute(normalizedFileName)) return isPathInside(pluginRoot, normalizedFileName) ? normalizedFileName : void 0;
1500
- const pluginBase = posix.basename(pluginRoot);
1501
- if (normalizedFileName !== pluginBase && !normalizedFileName.startsWith(`${pluginBase}/`)) return;
1502
- const relative = normalizedFileName === pluginBase ? "" : normalizedFileName.slice(pluginBase.length + 1);
1503
- const absolute = posix.resolve(pluginRoot, relative);
1504
- return isPathInside(pluginRoot, absolute) ? absolute : void 0;
1505
- }
1506
- function resolveSubPackageRoot$1(fileName, context) {
1507
- const normalized = posix.normalize(fileName);
1508
- return Array.from(context.subPackageRoots).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => normalized === root || normalized.startsWith(`${root}/`));
1509
- }
1510
- function classifyPackage(fileName, origin, context) {
1511
- if (fileName.startsWith(VIRTUAL_PREFIX)) {
1512
- const combination = fileName.slice(VIRTUAL_PREFIX.length).split("/")[0] || "shared";
1513
- return {
1514
- id: `virtual:${combination}`,
1515
- label: `共享虚拟包 ${combination}`,
1516
- type: "virtual"
1517
- };
1518
- }
1519
- const rootCandidate = resolveSubPackageRoot$1(fileName, context);
1520
- if (rootCandidate) {
1521
- const isIndependent = context.independentRoots.has(rootCandidate);
1522
- return {
1523
- id: rootCandidate,
1524
- label: `${isIndependent ? "独立分包" : "分包"} ${rootCandidate}`,
1525
- type: isIndependent || origin === "independent" ? "independent" : "subPackage"
1526
- };
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
+ });
1527
1622
  }
1623
+ const artifacts = [...files.values()].sort((a, b) => a.file.localeCompare(b.file));
1624
+ if (artifacts.length === 0) throw new Error("未生成可分析产物,不能把缺失输出计为零字节。");
1528
1625
  return {
1529
- id: "__main__",
1530
- label: "主包",
1531
- type: "main"
1532
- };
1533
- }
1534
- function normalizeModuleId(id) {
1535
- if (!id || id.includes(VIRTUAL_MODULE_INDICATOR)) return;
1536
- if (!posix.isAbsolute(id)) return;
1537
- return posix.normalize(id);
1538
- }
1539
- function resolveModuleSourceType(absoluteId, ctx) {
1540
- const { configService } = ctx;
1541
- const isNodeModule = absoluteId.includes("/node_modules/") || absoluteId.includes("\\node_modules\\");
1542
- const pluginRoot = configService.absolutePluginRoot;
1543
- const srcRoot = configService.absoluteSrcRoot;
1544
- const sourceType = classifyModuleSourceKind({
1545
- isNodeModule,
1546
- inSrc: isPathInside(srcRoot, absoluteId),
1547
- inPlugin: pluginRoot ? isPathInside(pluginRoot, absoluteId) : false
1548
- });
1549
- return {
1550
- source: configService.relativeAbsoluteSrcRoot(absoluteId),
1551
- sourceType
1552
- };
1553
- }
1554
- function resolveAssetSource(fileName, ctx) {
1555
- const { configService } = ctx;
1556
- const normalized = posix.normalize(fileName);
1557
- const srcCandidate = posix.isAbsolute(normalized) ? normalized : posix.resolve(configService.absoluteSrcRoot, normalized);
1558
- if (isPathInside(configService.absoluteSrcRoot, srcCandidate)) return {
1559
- absolute: srcCandidate,
1560
- source: configService.relativeAbsoluteSrcRoot(srcCandidate),
1561
- sourceType: "src"
1562
- };
1563
- const pluginAbsolute = resolvePluginAssetAbsolute(normalized, configService.absolutePluginRoot);
1564
- if (pluginAbsolute) return {
1565
- absolute: pluginAbsolute,
1566
- source: configService.relativeAbsoluteSrcRoot(pluginAbsolute),
1567
- 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
+ }
1568
1634
  };
1569
1635
  }
1570
1636
  //#endregion
@@ -1619,6 +1685,7 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
1619
1685
  type: "chunk",
1620
1686
  from: origin,
1621
1687
  size: typeof chunk.code === "string" ? Buffer.byteLength(chunk.code, "utf8") : void 0,
1688
+ sha256: createHash("sha256").update(chunk.code).digest("hex"),
1622
1689
  ...getCompressedSizes$1(chunk.code),
1623
1690
  isEntry: chunk.isEntry,
1624
1691
  modules: []
@@ -1626,10 +1693,15 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
1626
1693
  if (chunk.imports.length > 0) chunkEntry.imports = [...chunk.imports].sort((a, b) => a.localeCompare(b));
1627
1694
  if (chunk.dynamicImports.length > 0) chunkEntry.dynamicImports = [...chunk.dynamicImports].sort((a, b) => a.localeCompare(b));
1628
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);
1629
1697
  for (const [rawModuleId, info] of moduleEntries) {
1630
- const absoluteId = normalizeModuleId(rawModuleId);
1698
+ const generatedRuntime = rawModuleId === "\0rolldown/runtime.js";
1699
+ const absoluteId = generatedRuntime ? rawModuleId : normalizeModuleId(rawModuleId);
1631
1700
  if (!absoluteId) continue;
1632
- const { source, sourceType } = resolveModuleSourceType(absoluteId, ctx);
1701
+ const { source, sourceType } = generatedRuntime ? {
1702
+ source: "rolldown:runtime",
1703
+ sourceType: "workspace"
1704
+ } : resolveModuleSourceType(absoluteId, ctx);
1633
1705
  const moduleEntry = {
1634
1706
  id: absoluteId,
1635
1707
  source,
@@ -1652,6 +1724,7 @@ function processAsset(asset, origin, ctx, classifierContext, packages, modules)
1652
1724
  type: "asset",
1653
1725
  from: origin,
1654
1726
  size: assetBuffer?.byteLength,
1727
+ sha256: assetBuffer ? createHash("sha256").update(assetBuffer).digest("hex") : void 0,
1655
1728
  ...getCompressedSizes$1(assetBuffer)
1656
1729
  };
1657
1730
  let assetSource;
@@ -1672,7 +1745,16 @@ function processOutput(output, origin, ctx, classifierContext, packages, modules
1672
1745
  processChunk(item, origin, ctx, classifierContext, packages, modules);
1673
1746
  onArtifact?.(item.fileName, item.code);
1674
1747
  } else if (item.type === "asset") {
1675
- 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);
1676
1758
  onArtifact?.(item.fileName, item.source);
1677
1759
  }
1678
1760
  }
@@ -1777,7 +1859,7 @@ function summarizeSubPackages(metas) {
1777
1859
  }
1778
1860
  //#endregion
1779
1861
  //#region src/analyze/subpackages/index.ts
1780
- async function analyzeSubpackages(ctx, options) {
1862
+ async function analyzeBuild(ctx, options) {
1781
1863
  const { configService, scanService, buildService } = ctx;
1782
1864
  if (!configService || !scanService || !buildService) throw new Error("analyzeSubpackages 需要先初始化 configService、scanService 和 buildService。");
1783
1865
  await scanService.loadAppEntry();
@@ -1815,9 +1897,22 @@ async function analyzeSubpackages(ctx, options) {
1815
1897
  }
1816
1898
  expandVirtualModulePlacements(modules, packages, classifierContext);
1817
1899
  const subPackages = summarizeSubPackages(subPackageMetas);
1818
- 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,
1819
1914
  metadata: createAnalyzeMetadata(ctx.configService),
1820
- packages: summarizePackages(packages),
1915
+ packages: packageReports,
1821
1916
  modules: summarizeModules(modules),
1822
1917
  subPackages,
1823
1918
  components: analyzeComponentUsage({
@@ -1826,6 +1921,17 @@ async function analyzeSubpackages(ctx, options) {
1826
1921
  }),
1827
1922
  glassEasel: createGlassEaselAnalyzeResult(ctx)
1828
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
+ }
1829
1935
  }
1830
1936
  //#endregion
1831
1937
  //#region src/analyze/subpackages/history.ts
@@ -1897,6 +2003,7 @@ function formatAnalyzeBytes(bytes) {
1897
2003
  return `${value.toFixed(value >= 100 || unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
1898
2004
  }
1899
2005
  function formatDelta(bytes) {
2006
+ if (bytes === null) return "缺少体积测量";
1900
2007
  if (typeof bytes !== "number" || Number.isNaN(bytes) || bytes === 0) return "无变化";
1901
2008
  return `${bytes > 0 ? "+" : "-"}${formatAnalyzeBytes(Math.abs(bytes))}`;
1902
2009
  }
@@ -1906,77 +2013,8 @@ function getFileSize(file) {
1906
2013
  function getCompressedSize(file) {
1907
2014
  return file.brotliSize ?? file.gzipSize ?? 0;
1908
2015
  }
1909
- function getBudgetLimit(type, budgets) {
1910
- if (!budgets) return;
1911
- if (type === "main") return budgets.mainBytes;
1912
- if (type === "subPackage") return budgets.subPackageBytes;
1913
- if (type === "independent") return budgets.independentBytes;
1914
- }
1915
- function createPackageSizeMap(result) {
1916
- return new Map((result?.packages ?? []).map((pkg) => {
1917
- const totalBytes = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
1918
- return [pkg.id, totalBytes];
1919
- }));
1920
- }
1921
- function createFileKey(packageId, fileName) {
1922
- return `${packageId}\u0000${fileName}`;
1923
- }
1924
- function createFileSizeMap(result) {
1925
- const map = /* @__PURE__ */ new Map();
1926
- for (const pkg of result?.packages ?? []) for (const file of pkg.files) map.set(createFileKey(pkg.id, file.file), getFileSize(file));
1927
- return map;
1928
- }
1929
- function createPackageTypeMap(result) {
1930
- return new Map(result.packages.map((pkg) => [pkg.id, pkg.type]));
1931
- }
1932
- function createModuleByteMap(result) {
1933
- const map = /* @__PURE__ */ new Map();
1934
- for (const pkg of result.packages) for (const file of pkg.files) for (const mod of file.modules ?? []) {
1935
- const bytes = mod.bytes ?? mod.originalBytes ?? 0;
1936
- map.set(mod.id, Math.max(map.get(mod.id) ?? 0, bytes));
1937
- }
1938
- return map;
1939
- }
1940
- function classifyIncrementCategory(source, sourceType) {
1941
- if (source.includes("wevu") || source.includes("@weapp-vite/dashboard")) return "WeVu / runtime";
1942
- if (sourceType === "node_modules" || source.includes("node_modules")) return "第三方依赖";
1943
- if (sourceType === "workspace") return "工作区包";
1944
- if (sourceType === "plugin") return "插件生成";
1945
- if (source.endsWith(".wxss") || source.endsWith(".css") || source.endsWith(".scss")) return "样式资源";
1946
- if (source.endsWith(".wxml") || source.endsWith(".json")) return "页面结构";
1947
- return "业务源码";
1948
- }
1949
- function createIncrementAdvice(item) {
1950
- if (item.category === "第三方依赖") return "检查依赖是否只在必要分包引用,必要时收敛到公共入口或替换轻量实现。";
1951
- if (item.category === "WeVu / runtime") return "确认运行时能力是否被新页面引入,优先排查组件和 API 引用边界。";
1952
- if (item.category === "样式资源") return "检查样式重复、原子类生成范围和组件样式裁剪。";
1953
- if (item.type === "new-file" || item.type === "new-module") return "确认是否为本次需求必要新增,评估分包归属和懒加载边界。";
1954
- return "对比本次变更,优先查看新增引用、共享模块和大对象常量。";
1955
- }
1956
- function createModuleSizeMap(result) {
1957
- const map = /* @__PURE__ */ new Map();
1958
- for (const pkg of result?.packages ?? []) for (const file of pkg.files) for (const module of file.modules ?? []) {
1959
- const bytes = module.bytes ?? module.originalBytes ?? 0;
1960
- const existing = map.get(module.id);
1961
- if (existing && existing.bytes >= bytes) continue;
1962
- map.set(module.id, {
1963
- source: module.source,
1964
- sourceType: module.sourceType,
1965
- bytes,
1966
- packageLabel: pkg.label,
1967
- file: file.file
1968
- });
1969
- }
1970
- return map;
1971
- }
1972
- function createDuplicateAdvice(sourceType, packageIds, packageTypeMap, estimatedSavingBytes) {
1973
- if (packageIds.some((packageId) => packageTypeMap.get(packageId) === "independent")) return estimatedSavingBytes > 0 ? "含独立分包,先确认隔离要求,再评估是否抽公共入口。" : "含独立分包,重复可能来自隔离边界。";
1974
- if (sourceType === "node_modules") return "依赖被多个包带入,检查引用边界或考虑主包公共入口。";
1975
- if (sourceType === "src" || sourceType === "workspace") return "共享源码跨包重复,优先抽公共模块或调整分包归属。";
1976
- if (sourceType === "plugin") return "插件生成内容跨包重复,检查插件产物输出策略。";
1977
- return "检查该模块是否需要在多个包内重复存在。";
1978
- }
1979
2016
  function formatBudgetStatus(item) {
2017
+ if (item.status === "unknown") return "归因或产物字节不足,无法验收";
1980
2018
  if (item.status === "ok") return "正常";
1981
2019
  return `${item.status === "exceeded" ? "超预算" : "接近预算"} ${(item.ratio * 100).toFixed(1)}%`;
1982
2020
  }
@@ -1991,124 +2029,22 @@ function createActionItems(options) {
1991
2029
  if (actions.length === 0) actions.push("当前没有预算超限或高收益重复模块,保持观察即可。");
1992
2030
  return actions;
1993
2031
  }
1994
- function createAnalyzeBudgetCheck(result) {
1995
- const budgets = result.metadata?.budgets;
1996
- if (!budgets) return [];
1997
- const items = [];
1998
- const totalBytes = result.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
1999
- const warningRatio = budgets.warningRatio;
2000
- const createItem = (options) => {
2001
- const ratio = options.limitBytes > 0 ? options.currentBytes / options.limitBytes : 0;
2002
- const status = ratio >= 1 ? "exceeded" : ratio >= warningRatio ? "warning" : "ok";
2003
- return {
2004
- ...options,
2005
- ratio,
2006
- status
2007
- };
2008
- };
2009
- items.push(createItem({
2010
- id: "__total__",
2011
- label: "总包",
2012
- scope: "total",
2013
- currentBytes: totalBytes,
2014
- limitBytes: budgets.totalBytes
2015
- }));
2016
- for (const pkg of result.packages) {
2017
- const limitBytes = getBudgetLimit(pkg.type, budgets);
2018
- if (!limitBytes) continue;
2019
- items.push(createItem({
2020
- id: pkg.id,
2021
- label: pkg.label,
2022
- scope: pkg.type,
2023
- currentBytes: pkg.files.reduce((sum, file) => sum + getFileSize(file), 0),
2024
- limitBytes
2025
- }));
2026
- }
2027
- return items.sort((a, b) => b.ratio - a.ratio || a.label.localeCompare(b.label));
2028
- }
2029
- function createDuplicateModuleInsights(result) {
2030
- const moduleByteMap = createModuleByteMap(result);
2031
- const packageTypeMap = createPackageTypeMap(result);
2032
- return result.modules.filter((module) => module.packages.length > 1).map((module) => {
2033
- const bytes = moduleByteMap.get(module.id) ?? 0;
2034
- const packageIds = module.packages.map((pkg) => pkg.packageId);
2035
- const estimatedSavingBytes = bytes * Math.max(module.packages.length - 1, 0);
2036
- return {
2037
- id: module.id,
2038
- source: module.source,
2039
- sourceType: module.sourceType,
2040
- packageCount: module.packages.length,
2041
- bytes,
2042
- estimatedSavingBytes,
2043
- packages: packageIds,
2044
- advice: createDuplicateAdvice(module.sourceType, packageIds, packageTypeMap, estimatedSavingBytes)
2045
- };
2046
- }).sort((a, b) => b.estimatedSavingBytes - a.estimatedSavingBytes || b.packageCount - a.packageCount || a.source.localeCompare(b.source));
2047
- }
2048
- function createAnalyzeIncrementAttribution(result, previousResult) {
2049
- if (!previousResult) return [];
2050
- const previousFiles = createFileSizeMap(previousResult);
2051
- const previousModules = createModuleSizeMap(previousResult);
2052
- const currentModules = createModuleSizeMap(result);
2053
- const items = [];
2054
- for (const pkg of result.packages) for (const file of pkg.files) {
2055
- const currentBytes = getFileSize(file);
2056
- const previousBytes = previousFiles.get(createFileKey(pkg.id, file.file)) ?? 0;
2057
- const deltaBytes = currentBytes - previousBytes;
2058
- if (deltaBytes <= 0) continue;
2059
- const type = previousBytes > 0 ? "increased-file" : "new-file";
2060
- const item = {
2061
- key: `file:${pkg.id}:${file.file}`,
2062
- type,
2063
- label: file.file,
2064
- category: classifyIncrementCategory(file.source ?? file.file),
2065
- packageLabel: pkg.label,
2066
- file: file.file,
2067
- currentBytes,
2068
- previousBytes,
2069
- deltaBytes,
2070
- advice: ""
2071
- };
2072
- items.push({
2073
- ...item,
2074
- advice: createIncrementAdvice(item)
2075
- });
2076
- }
2077
- for (const [id, module] of currentModules) {
2078
- const previousBytes = previousModules.get(id)?.bytes ?? 0;
2079
- const deltaBytes = module.bytes - previousBytes;
2080
- if (deltaBytes <= 0) continue;
2081
- const type = previousBytes > 0 ? "increased-module" : "new-module";
2082
- const item = {
2083
- key: `module:${id}`,
2084
- type,
2085
- label: module.source,
2086
- category: classifyIncrementCategory(module.source, module.sourceType),
2087
- packageLabel: module.packageLabel,
2088
- file: module.file,
2089
- currentBytes: module.bytes,
2090
- previousBytes,
2091
- deltaBytes,
2092
- advice: ""
2093
- };
2094
- items.push({
2095
- ...item,
2096
- advice: createIncrementAdvice(item)
2097
- });
2098
- }
2099
- 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));
2100
2035
  }
2101
2036
  function createAnalyzeIncrementCategorySummary(items) {
2102
2037
  const map = /* @__PURE__ */ new Map();
2103
2038
  for (const item of items) {
2104
- const entry = map.get(item.category) ?? {
2105
- category: item.category,
2039
+ const category = item.category ?? "";
2040
+ const entry = map.get(category) ?? {
2041
+ category,
2106
2042
  count: 0,
2107
2043
  deltaBytes: 0
2108
2044
  };
2109
2045
  entry.count += 1;
2110
2046
  entry.deltaBytes += item.deltaBytes;
2111
- map.set(item.category, entry);
2047
+ map.set(category, entry);
2112
2048
  }
2113
2049
  return [...map.values()].sort((a, b) => b.deltaBytes - a.deltaBytes || b.count - a.count || a.category.localeCompare(b.category));
2114
2050
  }
@@ -2119,8 +2055,8 @@ function createAnalyzePrMarkdownReport(result, previousResult) {
2119
2055
  })));
2120
2056
  const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
2121
2057
  const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
2122
- const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
2123
- const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
2058
+ const comparison = previousResult ? createAnalyzeComparison(result, previousResult) : void 0;
2059
+ const incrementItems = createReportGrowth(comparison);
2124
2060
  const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
2125
2061
  const duplicateInsights = createDuplicateModuleInsights(result);
2126
2062
  const budgetItems = createAnalyzeBudgetCheck(result);
@@ -2136,7 +2072,7 @@ function createAnalyzePrMarkdownReport(result, previousResult) {
2136
2072
  return [
2137
2073
  "## weapp-vite analyze PR 摘要",
2138
2074
  "",
2139
- `- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)})`,
2075
+ `- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(comparison?.deltaBytes)})`,
2140
2076
  `- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
2141
2077
  `- 预算告警:${budgetIssues.length}`,
2142
2078
  `- 增量归因:${incrementItems.length > 0 ? `${incrementItems.length} 项正向增长` : "无正向增长"}`,
@@ -2180,11 +2116,11 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2180
2116
  const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
2181
2117
  const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
2182
2118
  const duplicateInsights = createDuplicateModuleInsights(result);
2183
- const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
2119
+ const comparison = previousResult ? createAnalyzeComparison(result, previousResult) : void 0;
2120
+ const incrementItems = createReportGrowth(comparison);
2184
2121
  const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
2185
2122
  const budgetItems = createAnalyzeBudgetCheck(result);
2186
- const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
2187
- const previousPackageSizes = createPackageSizeMap(previousResult);
2123
+ const packageDeltas = new Map(comparison?.packages.map((item) => [item.packageId, item.deltaBytes]));
2188
2124
  const budgets = result.metadata?.budgets;
2189
2125
  const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
2190
2126
  const actionItems = createActionItems({
@@ -2194,9 +2130,9 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2194
2130
  const packageRows = result.packages.map((pkg) => {
2195
2131
  const size = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
2196
2132
  const compressed = pkg.files.reduce((sum, file) => sum + getCompressedSize(file), 0);
2197
- const previousSize = previousPackageSizes.get(pkg.id);
2133
+ const delta = packageDeltas.has(pkg.id) ? packageDeltas.get(pkg.id) : 0;
2198
2134
  const budgetStatus = budgetItems.find((item) => item.id === pkg.id);
2199
- 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) : "正常"} |`;
2200
2136
  }).join("\n");
2201
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");
2202
2138
  const duplicateRows = duplicateInsights.slice(0, 10).map((module) => `| ${module.source} | ${module.sourceType} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
@@ -2212,7 +2148,7 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2212
2148
  "",
2213
2149
  `- 总产物体积:${formatAnalyzeBytes(totalBytes)}`,
2214
2150
  `- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
2215
- `- 较上次:${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)}`,
2151
+ `- 较上次:${formatDelta(comparison?.deltaBytes)}`,
2216
2152
  `- 包体数量:${result.packages.length}`,
2217
2153
  `- 源码模块:${result.modules.length}`,
2218
2154
  `- 跨包复用:${duplicateInsights.length}`,
@@ -2259,725 +2195,164 @@ function createAnalyzeMarkdownReport(result, previousResult) {
2259
2195
  ].join("\n");
2260
2196
  }
2261
2197
  //#endregion
2262
- //#region src/cli/analyze/dashboardDevframe/artifacts.ts
2263
- const MAX_DASHBOARD_FILE_CONTENT_BYTES = 2097152;
2264
- /** 保留同一次分析的产物内容,不读取或改写开发构建目录。 */
2265
- function createDashboardArtifactSnapshot() {
2266
- const files = /* @__PURE__ */ new Map();
2267
- let retainedBytes = 0;
2268
- return {
2269
- files,
2270
- capture(fileName, source) {
2271
- const key = fileName.replaceAll("\\", "/");
2272
- const previous = files.get(key);
2273
- if (previous?.content !== void 0) retainedBytes -= previous.size;
2274
- const size = typeof source === "string" ? Buffer.byteLength(source, "utf8") : source.byteLength;
2275
- if (size > 2097152) {
2276
- files.set(key, {
2277
- size,
2278
- error: `文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`
2279
- });
2280
- return;
2281
- }
2282
- if (retainedBytes + size > 33554432) {
2283
- files.set(key, {
2284
- size,
2285
- error: "分析产物内容超过快照容量上限,已拒绝读取。"
2286
- });
2287
- return;
2288
- }
2289
- const content = typeof source === "string" ? source : Buffer.from(source.buffer, source.byteOffset, source.byteLength).toString("utf8");
2290
- files.set(key, {
2291
- content,
2292
- size
2293
- });
2294
- retainedBytes += size;
2295
- }
2296
- };
2198
+ //#region src/cli/analyze/dashboard.ts
2199
+ function createInstallCommand(agent) {
2200
+ const resolved = resolveCommand(agent ?? "npm", "install", [ANALYZE_DASHBOARD_PACKAGE_NAME]);
2201
+ if (!resolved) return `npm install ${ANALYZE_DASHBOARD_PACKAGE_NAME}`;
2202
+ return `${resolved.command} ${resolved.args.join(" ")}`;
2297
2203
  }
2298
- //#endregion
2299
- //#region src/cli/analyze/dashboardDevframe/paths.ts
2300
- function normalizeDashboardRelativePath(value) {
2301
- return value.replaceAll("\\", "/");
2302
- }
2303
- function stripDashboardFileQuery(value) {
2304
- const queryIndex = value.indexOf("?");
2305
- return queryIndex === -1 ? value : value.slice(0, queryIndex);
2306
- }
2307
- function addDashboardAllowedPath(paths, value) {
2308
- if (!value || value.includes("\0")) return;
2309
- const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
2310
- if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
2311
- paths.add(normalizedPath);
2312
- }
2313
- function addDashboardAllowedSourcePath(paths, value, rootKind) {
2314
- if (!value || value.includes("\0")) return;
2315
- const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
2316
- if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
2317
- const rootKinds = paths.get(normalizedPath) ?? /* @__PURE__ */ new Set();
2318
- rootKinds.add(rootKind);
2319
- paths.set(normalizedPath, rootKinds);
2320
- }
2321
- function resolveDashboardSourceRootKind(sourceType) {
2322
- if (sourceType === "plugin") return "plugin";
2323
- if (sourceType === "workspace" || sourceType === "node_modules") return "project";
2324
- return "src";
2325
- }
2326
- function createDashboardContentAllowlist(result) {
2327
- const artifactPaths = /* @__PURE__ */ new Set();
2328
- const sourcePaths = /* @__PURE__ */ new Map();
2329
- for (const packageReport of result.packages) for (const file of packageReport.files) {
2330
- addDashboardAllowedPath(artifactPaths, file.file);
2331
- const fileSourceType = file.sourceType ?? file.modules?.find((module) => module.source === file.source)?.sourceType;
2332
- addDashboardAllowedSourcePath(sourcePaths, file.source, resolveDashboardSourceRootKind(fileSourceType));
2333
- for (const module of file.modules ?? []) addDashboardAllowedSourcePath(sourcePaths, module.source, module.sourceType === "src" ? "src" : module.sourceType === "plugin" ? "plugin" : "project");
2204
+ async function startAnalyzeDashboard(result, options) {
2205
+ const shutdown = getDevShutdownScope();
2206
+ const resolved = resolveDashboardRoot(options);
2207
+ if (!resolved) {
2208
+ logger_default.warn(`[weapp-vite ui] 未安装可选仪表盘包 ${colors.bold(colors.green(ANALYZE_DASHBOARD_PACKAGE_NAME))},已自动降级关闭 dashboard 能力。`);
2209
+ logger_default.info(`如需启用,请执行 ${colors.bold(colors.green(createInstallCommand(options.packageManagerAgent)))}`);
2210
+ return;
2334
2211
  }
2335
- return {
2336
- artifactPaths,
2337
- sourcePaths
2338
- };
2339
- }
2340
- function resolveDashboardContentPath(root, requestPath, options) {
2341
- if (!root || !requestPath || requestPath.includes("\0")) return;
2342
- const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
2343
- if (path.isAbsolute(normalizedRequestPath) || !options.allowedPaths.has(normalizedRequestPath)) return;
2344
- const rootPath = path.resolve(root);
2345
- const absolutePath = path.resolve(rootPath, normalizedRequestPath);
2346
- const relativePath = path.relative(rootPath, absolutePath);
2347
- const allowParent = options.allowParent === true;
2348
- if (!relativePath || !allowParent && (relativePath.startsWith("..") || path.isAbsolute(relativePath))) return;
2349
- return {
2350
- absolutePath,
2351
- allowParent,
2352
- relativePath: allowParent ? normalizedRequestPath : normalizeDashboardRelativePath(relativePath),
2353
- rootPath
2354
- };
2355
- }
2356
- function resolveDashboardSourceContentPaths(roots, requestPath, allowedPaths) {
2357
- const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
2358
- const rootKinds = allowedPaths.get(normalizedRequestPath);
2359
- if (!rootKinds || rootKinds.size !== 1) return [];
2360
- const [rootKind] = rootKinds;
2361
- if (rootKind === "project") {
2362
- const resolved = resolveDashboardContentPath(roots.projectRoot, normalizedRequestPath, {
2363
- allowParent: true,
2364
- allowedPaths: /* @__PURE__ */ new Set([normalizedRequestPath])
2365
- });
2366
- return resolved ? [resolved] : [];
2367
- }
2368
- if (rootKind === "plugin") {
2369
- if (!roots.pluginRoot) return [];
2370
- const pluginBase = path.basename(roots.pluginRoot);
2371
- if (normalizedRequestPath !== pluginBase && !normalizedRequestPath.startsWith(`${pluginBase}/`)) return [];
2372
- const pluginRelativePath = normalizedRequestPath === pluginBase ? "" : normalizedRequestPath.slice(pluginBase.length + 1);
2373
- const resolved = resolveDashboardContentPath(roots.pluginRoot, pluginRelativePath, { allowedPaths: /* @__PURE__ */ new Set([pluginRelativePath]) });
2374
- return resolved ? [{
2375
- ...resolved,
2376
- relativePath: normalizedRequestPath
2377
- }] : [];
2378
- }
2379
- if (!roots.srcRoot) return [];
2380
- const candidates = (normalizedRequestPath.startsWith("src/") ? [normalizedRequestPath, normalizedRequestPath.slice(4)] : [normalizedRequestPath]).flatMap((relativePath) => {
2381
- const resolved = resolveDashboardContentPath(roots.srcRoot, relativePath, { allowedPaths: /* @__PURE__ */ new Set([relativePath]) });
2382
- return resolved ? [{
2383
- ...resolved,
2384
- relativePath: normalizedRequestPath
2385
- }] : [];
2212
+ const { root, configFile } = resolved;
2213
+ const { ANALYZE_DASHBOARD_DEVFRAME_BASE, ANALYZE_DASHBOARD_HUB_BASE, createAnalyzeDashboardViteBridge } = await import("./dashboardViteBridge-Dy3nAD4m.mjs");
2214
+ const devframe = createAnalyzeDashboardDevframe({
2215
+ clientAssets: options.uiHost === "hub" ? root : void 0,
2216
+ snapshot: {
2217
+ current: result,
2218
+ previous: options.previousResult ?? null,
2219
+ artifacts: options.artifacts
2220
+ },
2221
+ initialEvents: [{
2222
+ kind: "command",
2223
+ level: "success",
2224
+ title: options.watch ? "dashboard watch session started" : "dashboard static session started",
2225
+ detail: options.watch ? "weapp-vite UI 已进入实时分析模式,后续 analyze 结果会继续推送到 dashboard。" : "weapp-vite UI 已进入静态分析模式,当前页面展示的是一次性分析结果。",
2226
+ tags: options.watch ? ["watch", "analyze"] : ["static", "analyze"]
2227
+ }, ...options.initialEvents ?? []],
2228
+ roots: {
2229
+ pluginRoot: options.pluginRoot,
2230
+ projectRoot: options.cwd,
2231
+ srcRoot: options.srcRoot ?? (options.cwd ? path.resolve(options.cwd, "src") : void 0)
2232
+ }
2386
2233
  });
2387
- return [...new Map(candidates.map((candidate) => [candidate.absolutePath, candidate])).values()];
2388
- }
2389
- function resolveDashboardArtifactPath(requestPath, allowedPaths) {
2390
- const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
2391
- if (!normalizedPath || normalizedPath.includes("\0") || path.isAbsolute(normalizedPath) || path.posix.normalize(normalizedPath).startsWith("..") || !allowedPaths.has(normalizedPath)) return;
2392
- return normalizedPath;
2393
- }
2394
- //#endregion
2395
- //#region src/cli/analyze/dashboardDevframe/content.ts
2396
- function isPathWithin(rootPath, targetPath) {
2397
- const relativePath = path.relative(rootPath, targetPath);
2398
- return !relativePath || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
2399
- }
2400
- function resolveInspectionRoot(resolved) {
2401
- let inspectionRoot = resolved.rootPath;
2402
- if (!resolved.allowParent) return inspectionRoot;
2403
- while (!isPathWithin(inspectionRoot, resolved.absolutePath)) {
2404
- const parent = path.dirname(inspectionRoot);
2405
- if (parent === inspectionRoot) return;
2406
- inspectionRoot = parent;
2407
- }
2408
- return inspectionRoot;
2409
- }
2410
- async function assertDashboardPathHasNoSymlink(resolved) {
2411
- const inspectionRoot = resolveInspectionRoot(resolved);
2412
- if (!inspectionRoot) throw new Error("文件路径超出允许范围。");
2413
- const relativePath = path.relative(inspectionRoot, resolved.absolutePath);
2414
- let currentPath = inspectionRoot;
2415
- for (const segment of relativePath.split(path.sep).filter(Boolean)) {
2416
- currentPath = path.join(currentPath, segment);
2417
- if ((await fs.promises.lstat(currentPath)).isSymbolicLink()) throw new Error("文件路径包含不允许的符号链接。");
2418
- }
2419
- }
2420
- function isSameOpenedFile(pathStat, handleStat) {
2421
- if (pathStat.dev !== handleStat.dev) return false;
2422
- return pathStat.ino === 0 || handleStat.ino === 0 || pathStat.ino === handleStat.ino;
2423
- }
2424
- async function openDashboardFile(resolved) {
2425
- await assertDashboardPathHasNoSymlink(resolved);
2426
- const flags = process.platform === "win32" ? fs.constants.O_RDONLY : fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
2427
- const handle = await fs.promises.open(resolved.absolutePath, flags);
2234
+ const bridge = createAnalyzeDashboardViteBridge(devframe, {
2235
+ projectRoot: options.cwd ?? process.cwd(),
2236
+ uiHost: options.uiHost
2237
+ });
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
+ };
2428
2247
  try {
2429
- await assertDashboardPathHasNoSymlink(resolved);
2430
- const [pathStat, handleStat] = await Promise.all([fs.promises.stat(resolved.absolutePath), handle.stat()]);
2431
- if (!isSameOpenedFile(pathStat, handleStat)) throw new Error("文件路径在读取期间发生变化。");
2432
- return handle;
2248
+ server = await createDevViteServer({
2249
+ root,
2250
+ base: ANALYZE_DASHBOARD_DEVFRAME_BASE,
2251
+ configFile: configFile ?? false,
2252
+ clearScreen: false,
2253
+ appType: "spa",
2254
+ publicDir: false,
2255
+ plugins: [{
2256
+ name: "weapp-vite:dashboard-lifetime",
2257
+ enforce: "pre",
2258
+ configureServer(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
+ });
2269
+ }
2270
+ }, bridge],
2271
+ server: {
2272
+ host: "127.0.0.1",
2273
+ port: 0,
2274
+ watch: { ignored: ["**/*"] }
2275
+ },
2276
+ logLevel: "error"
2277
+ });
2278
+ if (!shutdown?.stopping) await server.listen(0);
2433
2279
  } catch (error) {
2434
- await handle.close();
2280
+ const cleanupErrors = await closeResources();
2281
+ if (cleanupErrors.length) throw new AggregateError([error, ...cleanupErrors], "Dashboard 启动及资源清理失败", { cause: error });
2435
2282
  throw error;
2436
2283
  }
2437
- }
2438
- async function readDashboardFileHandle(handle) {
2439
- const stat = await handle.stat();
2440
- if (!stat.isFile()) throw new Error("目标路径不是文件。");
2441
- if (stat.size > 2097152) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
2442
- let content = Buffer.allocUnsafe(Math.min(Math.max(stat.size + 1, 1), 2097153));
2443
- let offset = 0;
2444
- while (true) {
2445
- if (offset === content.length) {
2446
- if (content.length === 2097153) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
2447
- const expanded = Buffer.allocUnsafe(Math.min(content.length * 2, 2097153));
2448
- content.copy(expanded, 0, 0, offset);
2449
- content = expanded;
2450
- }
2451
- const { bytesRead } = await handle.read(content, offset, content.length - offset, offset);
2452
- if (bytesRead === 0) break;
2453
- offset += bytesRead;
2454
- if (offset > 2097152) throw new Error(`文件超过 ${MAX_DASHBOARD_FILE_CONTENT_BYTES} 字节,已拒绝读取。`);
2455
- }
2456
- return {
2457
- content: content.subarray(0, offset).toString("utf8"),
2458
- size: offset
2284
+ const activeServer = server;
2285
+ const urls = [...activeServer.resolvedUrls?.local ?? [], ...activeServer.resolvedUrls?.network ?? []];
2286
+ const authCode = refreshTempAuthCode();
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
+ });
2291
+ let closing;
2292
+ let resolveExit;
2293
+ const waitPromise = new Promise((resolve) => {
2294
+ resolveExit = resolve;
2295
+ });
2296
+ const signals = ["SIGINT", "SIGTERM"];
2297
+ let onExit;
2298
+ let releaseOwnership;
2299
+ const finish = () => {
2300
+ for (const signal of signals) process.removeListener(signal, onExit);
2301
+ releaseOwnership?.();
2302
+ resolveExit();
2459
2303
  };
2460
- }
2461
- function resolveDashboardFileLanguage(filePath) {
2462
- const extension = path.extname(filePath).toLowerCase();
2463
- if ([
2464
- ".js",
2465
- ".mjs",
2466
- ".cjs",
2467
- ".wxs",
2468
- ".sjs"
2469
- ].includes(extension)) return "javascript";
2470
- if ([
2471
- ".ts",
2472
- ".mts",
2473
- ".cts"
2474
- ].includes(extension)) return "typescript";
2475
- if ([".json", ".map"].includes(extension)) return "json";
2476
- if ([
2477
- ".css",
2478
- ".wxss",
2479
- ".scss",
2480
- ".sass",
2481
- ".less"
2482
- ].includes(extension)) return "css";
2483
- if ([
2484
- ".vue",
2485
- ".wxml",
2486
- ".html"
2487
- ].includes(extension)) return "html";
2488
- return "plaintext";
2489
- }
2490
- function normalizeDashboardFileRequest(input) {
2491
- if (!input || typeof input !== "object" || !("kind" in input) || !("path" in input)) throw new Error("必须传入合法的 kind 和相对路径。");
2492
- const kind = input.kind;
2493
- const requestPath = input.path;
2494
- if (kind !== "source" && kind !== "artifact" || typeof requestPath !== "string") throw new Error("必须传入合法的 kind 和相对路径。");
2495
- return {
2496
- kind,
2497
- path: requestPath
2304
+ onHostClose = () => {
2305
+ if (!closing) finish();
2498
2306
  };
2499
- }
2500
- async function readAllowedDashboardFile(input, roots, allowlist, artifacts) {
2501
- const request = normalizeDashboardFileRequest(input);
2502
- if (request.kind === "artifact") {
2503
- const filePath = resolveDashboardArtifactPath(request.path, allowlist.artifactPaths);
2504
- if (!filePath) throw new Error("必须传入合法的 kind 和相对路径。");
2505
- const file = artifacts.get(filePath);
2506
- if (!file) throw new Error("分析快照中没有此产物内容。");
2507
- if (file.error || file.content === void 0) throw new Error(file.error ?? "分析快照中没有此产物内容。");
2508
- return {
2509
- kind: request.kind,
2510
- path: filePath,
2511
- language: resolveDashboardFileLanguage(filePath),
2512
- size: file.size,
2513
- content: file.content
2514
- };
2307
+ const close = () => {
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);
2312
+ return closing;
2313
+ };
2314
+ onExit = async () => {
2315
+ await close().catch((error) => logger_default.error(error));
2316
+ };
2317
+ if (!shutdown) for (const signal of signals) process.once(signal, onExit);
2318
+ const handle = {
2319
+ update: devframe.update,
2320
+ emitRuntimeEvents: devframe.emitRuntimeEvents,
2321
+ waitForExit: () => waitPromise,
2322
+ close,
2323
+ urls: authenticatedUrls
2324
+ };
2325
+ releaseOwnership = shutdown?.own(close);
2326
+ if (shutdown?.stopping) {
2327
+ await close();
2328
+ return;
2515
2329
  }
2516
- const resolvedCandidates = resolveDashboardSourceContentPaths(roots, request.path, allowlist.sourcePaths);
2517
- if (resolvedCandidates.length === 0) throw new Error("必须传入合法的 kind 和相对路径。");
2518
- const openedCandidates = [];
2519
- try {
2520
- for (const resolved of resolvedCandidates) try {
2521
- openedCandidates.push({
2522
- handle: await openDashboardFile(resolved),
2523
- resolved
2524
- });
2525
- } catch (error) {
2526
- if ((typeof error === "object" && error && "code" in error ? String(error.code) : "") !== "ENOENT") throw error;
2330
+ if (options.watch) {
2331
+ if (!options.silentStartupLog) {
2332
+ logger_default.info("weapp-vite UI 已启动(分析视图,实时模式),按 Ctrl+C 退出。");
2333
+ for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
2527
2334
  }
2528
- if (openedCandidates.length === 0) throw new Error("文件不存在。");
2529
- if (openedCandidates.length > 1) throw new Error("源码路径存在多个候选文件,已拒绝读取。");
2530
- const [{ handle, resolved }] = openedCandidates;
2531
- const file = await readDashboardFileHandle(handle);
2532
- return {
2533
- kind: request.kind,
2534
- path: resolved.relativePath,
2535
- language: resolveDashboardFileLanguage(resolved.relativePath),
2536
- size: file.size,
2537
- content: file.content
2538
- };
2539
- } catch (error) {
2540
- if (error instanceof Error && (error.message === "文件不存在。" || error.message === "源码路径存在多个候选文件,已拒绝读取。" || error.message === "目标路径不是文件。" || error.message.startsWith("文件超过 ") || error.message.startsWith("文件路径"))) throw error;
2541
- throw new Error("读取文件失败。");
2335
+ return handle;
2336
+ }
2337
+ if (!options.silentStartupLog) {
2338
+ logger_default.info("weapp-vite UI 已启动(分析视图,静态模式),按 Ctrl+C 退出。");
2339
+ for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
2340
+ }
2341
+ await waitPromise;
2342
+ }
2343
+ //#endregion
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);
2542
2352
  } finally {
2543
- await Promise.allSettled(openedCandidates.map((candidate) => candidate.handle.close()));
2353
+ process.stdout.write = stdoutWrite;
2544
2354
  }
2545
2355
  }
2546
- function createDashboardFileReader(roots, result, artifacts) {
2547
- let allowlist = createDashboardContentAllowlist(result);
2548
- let currentArtifacts = artifacts;
2549
- let disposed = false;
2550
- return {
2551
- read: async (input) => {
2552
- if (disposed) throw new Error("Dashboard 文件读取会话已关闭。");
2553
- return await readAllowedDashboardFile(input, roots, allowlist, currentArtifacts);
2554
- },
2555
- update(nextResult, nextArtifacts) {
2556
- allowlist = createDashboardContentAllowlist(nextResult);
2557
- currentArtifacts = nextArtifacts;
2558
- },
2559
- dispose() {
2560
- disposed = true;
2561
- currentArtifacts = /* @__PURE__ */ new Map();
2562
- allowlist.artifactPaths.clear();
2563
- allowlist.sourcePaths.clear();
2564
- }
2565
- };
2566
- }
2567
- //#endregion
2568
- //#region src/cli/analyze/dashboardDevframe/index.ts
2569
- const DEVFRAME_ID = "weapp-vite";
2570
- const MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS = 65536;
2571
- const STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE = "Analyze revision 已变化,请重新获取 Dashboard 状态。";
2572
- const rejectDashboardSharedStateSet = defineRpcFunction({
2573
- name: "devframe:rpc:server-state:set",
2574
- type: "action",
2575
- handler: () => {
2576
- throw new Error("Dashboard Devframe shared state 只允许服务端修改。");
2577
- }
2578
- });
2579
- const rejectDashboardSharedStatePatch = defineRpcFunction({
2580
- name: "devframe:rpc:server-state:patch",
2581
- type: "action",
2582
- handler: () => {
2583
- throw new Error("Dashboard Devframe shared state 只允许服务端修改。");
2584
- }
2585
- });
2586
- function serializeDashboardAnalyzePayload(result) {
2587
- const value = JSON.stringify(result);
2588
- return {
2589
- descriptor: {
2590
- characters: value.length,
2591
- hash: createHash("sha256").update(value).digest("hex"),
2592
- pages: Math.max(1, Math.ceil(value.length / MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS))
2593
- },
2594
- source: result,
2595
- value
2596
- };
2597
- }
2598
- function serializeDashboardAnalyzeSnapshot(snapshot, cached) {
2599
- const previous = snapshot.previous ? cached?.current.source === snapshot.previous ? cached.current : cached?.previous?.source === snapshot.previous ? cached.previous : serializeDashboardAnalyzePayload(snapshot.previous) : null;
2600
- return {
2601
- current: cached?.current.source === snapshot.current ? cached.current : serializeDashboardAnalyzePayload(snapshot.current),
2602
- previous
2603
- };
2604
- }
2605
- function createDashboardState(revision, snapshot, runtimeEvents) {
2606
- return {
2607
- analyze: {
2608
- current: snapshot.current.descriptor,
2609
- previous: snapshot.previous?.descriptor ?? null
2610
- },
2611
- revision,
2612
- runtimeEvents: [...runtimeEvents]
2613
- };
2614
- }
2615
- function normalizeAnalyzePageRequest(input) {
2616
- if (!input || typeof input !== "object") throw new Error("必须传入合法的 Analyze 分页请求。");
2617
- if (!("revision" in input) || !("target" in input) || !("index" in input)) throw new Error("必须传入合法的 Analyze 分页请求。");
2618
- const revision = input.revision;
2619
- const target = input.target;
2620
- const index = input.index;
2621
- if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 0 || target !== "current" && target !== "previous" || typeof index !== "number" || !Number.isSafeInteger(index) || index < 0) throw new Error("必须传入合法的 Analyze 分页请求。");
2622
- return {
2623
- revision,
2624
- target,
2625
- index
2626
- };
2627
- }
2628
- function readDashboardAnalyzePage(input, revision, snapshot) {
2629
- const request = normalizeAnalyzePageRequest(input);
2630
- if (request.revision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
2631
- const payload = request.target === "current" ? snapshot.current : snapshot.previous;
2632
- if (!payload || request.index >= payload.descriptor.pages) throw new Error("Analyze 分页不存在。");
2633
- const start = request.index * MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS;
2634
- return {
2635
- content: payload.value.slice(start, start + MAX_DASHBOARD_ANALYZE_PAGE_CHARACTERS),
2636
- descriptor: payload.descriptor,
2637
- index: request.index,
2638
- revision,
2639
- target: request.target
2640
- };
2641
- }
2642
- function createAnalyzeDashboardDevframe(options) {
2643
- let revision = 0;
2644
- let disposed = false;
2645
- let serializedSnapshot = serializeDashboardAnalyzeSnapshot(options.getAnalyzeSnapshot());
2646
- let broadcastDashboardState;
2647
- const initialSnapshot = options.getAnalyzeSnapshot();
2648
- const fileReader = createDashboardFileReader(options.roots, initialSnapshot.current, initialSnapshot.artifacts);
2649
- const getDashboardState = defineRpcFunction({
2650
- name: "get-dashboard-state",
2651
- type: "query",
2652
- jsonSerializable: true,
2653
- handler: () => createDashboardState(revision, serializedSnapshot, options.getRuntimeEvents())
2654
- });
2655
- const getAnalyzePage = defineRpcFunction({
2656
- name: "get-analyze-page",
2657
- type: "query",
2658
- jsonSerializable: true,
2659
- handler: (input) => readDashboardAnalyzePage(input, revision, serializedSnapshot)
2660
- });
2661
- const readDashboardFile = defineRpcFunction({
2662
- name: "read-dashboard-file",
2663
- type: "query",
2664
- jsonSerializable: true,
2665
- handler: async (input) => {
2666
- if (!input || typeof input !== "object" || !("revision" in input) || typeof input.revision !== "number" || !Number.isSafeInteger(input.revision) || input.revision < 0) throw new Error("必须传入合法的文件读取 revision。");
2667
- const requestedRevision = input.revision;
2668
- if (disposed || requestedRevision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
2669
- const content = await fileReader.read(input);
2670
- if (disposed || requestedRevision !== revision) throw new Error(STALE_DASHBOARD_ANALYZE_REVISION_MESSAGE);
2671
- return content;
2672
- }
2673
- });
2674
- return {
2675
- definition: defineDevframe({
2676
- id: DEVFRAME_ID,
2677
- name: "weapp-vite",
2678
- version: VERSION,
2679
- packageName: "weapp-vite",
2680
- importMetaUrl: import.meta.url,
2681
- homepage: "https://vite.weapp.dev/",
2682
- description: "weapp-vite 构建分析与小程序开发工具。",
2683
- icon: "ph:rocket-launch-duotone",
2684
- async setup(ctx) {
2685
- ctx.rpc.register(rejectDashboardSharedStateSet, true);
2686
- ctx.rpc.register(rejectDashboardSharedStatePatch, true);
2687
- const dashboard = ctx.scope(DEVFRAME_ID);
2688
- dashboard.rpc.register(getDashboardState);
2689
- dashboard.rpc.register(getAnalyzePage);
2690
- dashboard.rpc.register(readDashboardFile);
2691
- broadcastDashboardState = async () => {
2692
- await dashboard.rpc.broadcast({
2693
- method: "dashboard-state-updated",
2694
- args: [createDashboardState(revision, serializedSnapshot, options.getRuntimeEvents())],
2695
- event: true
2696
- });
2697
- };
2698
- }
2699
- }),
2700
- notifyAnalyzeUpdate() {
2701
- if (disposed) return;
2702
- revision += 1;
2703
- const snapshot = options.getAnalyzeSnapshot();
2704
- serializedSnapshot = serializeDashboardAnalyzeSnapshot(snapshot, serializedSnapshot);
2705
- fileReader.update(snapshot.current, snapshot.artifacts);
2706
- broadcastDashboardState?.();
2707
- },
2708
- syncRuntimeEvents() {
2709
- broadcastDashboardState?.();
2710
- },
2711
- dispose() {
2712
- disposed = true;
2713
- fileReader.dispose();
2714
- broadcastDashboardState = void 0;
2715
- }
2716
- };
2717
- }
2718
- //#endregion
2719
- //#region src/cli/analyze/dashboardViteBridge.ts
2720
- const ANALYZE_DASHBOARD_DEVFRAME_BASE = "/__weapp-vite/";
2721
- function createAnalyzeDashboardViteBridge(definition) {
2722
- let instance;
2723
- let closing;
2724
- const closeInstance = async () => {
2725
- if (closing) return await closing;
2726
- const current = instance;
2727
- instance = void 0;
2728
- if (!current) return;
2729
- closing = current.close().finally(() => {
2730
- closing = void 0;
2731
- });
2732
- await closing;
2733
- };
2734
- return {
2735
- name: "weapp-vite:dashboard-devframe",
2736
- apply: "serve",
2737
- async configureServer(server) {
2738
- await closeInstance();
2739
- const httpServer = server.httpServer instanceof Server ? server.httpServer : void 0;
2740
- const created = initDevframe(definition, {
2741
- base: ANALYZE_DASHBOARD_DEVFRAME_BASE,
2742
- distDir: false,
2743
- ...httpServer ? { server: httpServer } : {
2744
- host: "127.0.0.1",
2745
- ws: { sidecar: true }
2746
- },
2747
- allowedOrigins: [],
2748
- auth: true,
2749
- mcp: false
2750
- });
2751
- server.middlewares.use(created.nodeMiddleware);
2752
- try {
2753
- await created.ready;
2754
- instance = created;
2755
- } catch (error) {
2756
- await created.close();
2757
- throw error;
2758
- }
2759
- server.httpServer?.once("close", () => {
2760
- closeInstance();
2761
- });
2762
- },
2763
- async closeBundle() {
2764
- await closeInstance();
2765
- }
2766
- };
2767
- }
2768
- //#endregion
2769
- //#region src/cli/analyze/dashboard.ts
2770
- const ANALYZE_DASHBOARD_PACKAGE_NAME = "@weapp-vite/dashboard";
2771
- const require = createRequire(import.meta.url);
2772
- function createInstallCommand(agent) {
2773
- const resolved = resolveCommand(agent ?? "npm", "install", [ANALYZE_DASHBOARD_PACKAGE_NAME]);
2774
- if (!resolved) return `npm install ${ANALYZE_DASHBOARD_PACKAGE_NAME}`;
2775
- return `${resolved.command} ${resolved.args.join(" ")}`;
2776
- }
2777
- function formatEventTimestamp(date = /* @__PURE__ */ new Date()) {
2778
- return date.toLocaleTimeString("zh-CN", { hour12: false });
2779
- }
2780
- function createDashboardRuntimeEvent(input) {
2781
- return {
2782
- id: `dashboard:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
2783
- kind: input.kind,
2784
- level: input.level,
2785
- title: input.title,
2786
- detail: input.detail,
2787
- timestamp: formatEventTimestamp(),
2788
- source: input.source ?? "weapp-vite",
2789
- durationMs: input.durationMs,
2790
- tags: input.tags,
2791
- profile: input.profile
2792
- };
2793
- }
2794
- function readDashboardManifest(packageJsonPath) {
2795
- try {
2796
- return parseCommentJson(fs.readFileSync(packageJsonPath, "utf8"));
2797
- } catch {
2798
- return;
2799
- }
2800
- }
2801
- function resolveDashboardDistRoot(packageRoot, manifest) {
2802
- const distDir = manifest?.weappViteDashboard?.distDir ?? "dist";
2803
- const distRoot = path.resolve(packageRoot, distDir);
2804
- if (!fs.existsSync(distRoot)) return;
2805
- return { root: distRoot };
2806
- }
2807
- function resolveDashboardDevRoot(packageRoot, manifest) {
2808
- const devRoot = manifest?.weappViteDashboard?.devRoot;
2809
- const devConfigFile = manifest?.weappViteDashboard?.devConfigFile;
2810
- if (!devRoot || !devConfigFile) return;
2811
- const root = path.resolve(packageRoot, devRoot);
2812
- const configFile = path.resolve(root, devConfigFile);
2813
- if (!fs.existsSync(root) || !fs.existsSync(configFile)) return;
2814
- return {
2815
- root,
2816
- configFile
2817
- };
2818
- }
2819
- function resolveDashboardRoot(options) {
2820
- const resolvePaths = options?.cwd && options.cwd !== process.cwd() ? [options.cwd, process.cwd()] : options?.cwd ? [options.cwd] : void 0;
2821
- let dashboardPackageRoot;
2822
- let dashboardManifest;
2823
- try {
2824
- const dashboardPackageJsonPath = require.resolve(`${ANALYZE_DASHBOARD_PACKAGE_NAME}/package.json`, { paths: resolvePaths });
2825
- dashboardPackageRoot = path.dirname(dashboardPackageJsonPath);
2826
- dashboardManifest = readDashboardManifest(dashboardPackageJsonPath);
2827
- } catch {
2828
- dashboardPackageRoot = void 0;
2829
- dashboardManifest = void 0;
2830
- }
2831
- if (dashboardPackageRoot) {
2832
- const devResolved = resolveDashboardDevRoot(dashboardPackageRoot, dashboardManifest);
2833
- const distResolved = resolveDashboardDistRoot(dashboardPackageRoot, dashboardManifest);
2834
- const resolved = options?.watch ? devResolved ?? distResolved : distResolved ?? devResolved;
2835
- if (resolved) return resolved;
2836
- }
2837
- logger_default.warn(`[weapp-vite ui] 未安装可选仪表盘包 ${colors.bold(colors.green(ANALYZE_DASHBOARD_PACKAGE_NAME))},已自动降级关闭 dashboard 能力。`);
2838
- logger_default.info(`如需启用,请执行 ${colors.bold(colors.green(createInstallCommand(options?.packageManagerAgent)))}`);
2839
- }
2840
- async function waitForServerExit(server) {
2841
- let resolved = false;
2842
- const cleanup = async () => {
2843
- if (resolved) return;
2844
- resolved = true;
2845
- try {
2846
- await server.close();
2847
- } catch (error) {
2848
- logger_default.error(error);
2849
- }
2850
- };
2851
- const signals = ["SIGINT", "SIGTERM"];
2852
- await new Promise((resolvePromise) => {
2853
- const resolveOnce = async () => {
2854
- await cleanup();
2855
- signals.forEach((signal) => {
2856
- process.removeListener(signal, resolveOnce);
2857
- });
2858
- resolvePromise();
2859
- };
2860
- signals.forEach((signal) => {
2861
- process.once(signal, resolveOnce);
2862
- });
2863
- server.httpServer?.once("close", resolveOnce);
2864
- });
2865
- }
2866
- async function startAnalyzeDashboard(result, options) {
2867
- const resolved = resolveDashboardRoot(options);
2868
- if (!resolved) return;
2869
- const { root, configFile } = resolved;
2870
- const state = {
2871
- current: result,
2872
- previous: options.previousResult ?? null,
2873
- artifacts: options.artifacts
2874
- };
2875
- const runtimeEvents = { current: [createDashboardRuntimeEvent({
2876
- kind: "command",
2877
- level: "success",
2878
- title: options?.watch ? "dashboard watch session started" : "dashboard static session started",
2879
- detail: options?.watch ? "weapp-vite UI 已进入实时分析模式,后续 analyze 结果会继续推送到 dashboard。" : "weapp-vite UI 已进入静态分析模式,当前页面展示的是一次性分析结果。",
2880
- tags: options?.watch ? ["watch", "analyze"] : ["static", "analyze"]
2881
- }), ...(options?.initialEvents ?? []).map((event) => createDashboardRuntimeEvent(event))] };
2882
- const devframe = createAnalyzeDashboardDevframe({
2883
- getAnalyzeSnapshot: () => state,
2884
- getRuntimeEvents: () => runtimeEvents.current,
2885
- roots: {
2886
- pluginRoot: options?.pluginRoot,
2887
- projectRoot: options?.cwd,
2888
- srcRoot: options?.srcRoot ?? (options?.cwd ? path.resolve(options.cwd, "src") : void 0)
2889
- }
2890
- });
2891
- const plugins = [createAnalyzeDashboardViteBridge(devframe.definition)];
2892
- const serverOptions = {
2893
- root,
2894
- configFile: configFile ?? false,
2895
- clearScreen: false,
2896
- appType: "spa",
2897
- publicDir: false,
2898
- plugins,
2899
- server: {
2900
- host: "127.0.0.1",
2901
- port: 0,
2902
- watch: { ignored: ["**/*"] }
2903
- },
2904
- logLevel: "error"
2905
- };
2906
- const server = await createServer(serverOptions);
2907
- const requestedPort = typeof serverOptions.server?.port === "number" ? serverOptions.server.port : void 0;
2908
- await server.listen(requestedPort);
2909
- const urls = (() => {
2910
- const resolved = server.resolvedUrls;
2911
- if (!resolved) return [];
2912
- return [...resolved.local ?? [], ...resolved.network ?? []];
2913
- })();
2914
- const authCode = refreshTempAuthCode();
2915
- const authenticatedUrls = urls.map((url) => buildOtpAuthUrl(url, authCode));
2916
- let closed = false;
2917
- const waitPromise = waitForServerExit(server).then(() => {
2918
- closed = true;
2919
- state.artifacts = /* @__PURE__ */ new Map();
2920
- devframe.dispose();
2921
- });
2922
- const emitRuntimeEvents = (events) => {
2923
- if (events.length === 0) return;
2924
- const nextEvents = events.map((event) => createDashboardRuntimeEvent(event));
2925
- runtimeEvents.current = [...nextEvents, ...runtimeEvents.current].slice(0, 24);
2926
- devframe.syncRuntimeEvents();
2927
- };
2928
- const handle = {
2929
- async update(nextResult, artifacts, previousResult) {
2930
- if (closed) return;
2931
- state.previous = previousResult ?? state.current;
2932
- state.current = nextResult;
2933
- state.artifacts = artifacts;
2934
- emitRuntimeEvents([{
2935
- kind: "build",
2936
- level: "info",
2937
- title: "analyze payload refreshed",
2938
- detail: `已推送新的 analyze 结果,当前包含 ${nextResult.packages.length} 个包与 ${nextResult.modules.length} 个模块。`,
2939
- tags: ["analyze", "refresh"]
2940
- }]);
2941
- devframe.notifyAnalyzeUpdate();
2942
- },
2943
- emitRuntimeEvents,
2944
- waitForExit: () => waitPromise,
2945
- close: async () => {
2946
- closed = true;
2947
- await server.close();
2948
- },
2949
- urls: authenticatedUrls
2950
- };
2951
- if (options?.watch) {
2952
- if (!options.silentStartupLog) {
2953
- logger_default.info("weapp-vite UI 已启动(分析视图,实时模式),按 Ctrl+C 退出。");
2954
- for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
2955
- }
2956
- return handle;
2957
- }
2958
- if (!options?.silentStartupLog) {
2959
- logger_default.info("weapp-vite UI 已启动(分析视图,静态模式),按 Ctrl+C 退出。");
2960
- for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
2961
- }
2962
- await waitPromise;
2963
- }
2964
- //#endregion
2965
- //#region src/cli/processCleanup.ts
2966
- function isSassEmbeddedChild(handle) {
2967
- if (!handle || typeof handle !== "object") return false;
2968
- const child = handle;
2969
- const spawnfile = typeof child.spawnfile === "string" ? child.spawnfile : "";
2970
- const spawnargs = Array.isArray(child.spawnargs) ? child.spawnargs : [];
2971
- return Boolean("kill" in handle && "spawnfile" in handle && (spawnfile.includes("sass-embedded") || spawnfile.includes("dart-sass") && spawnargs.includes("--embedded")));
2972
- }
2973
- function terminateStaleSassEmbeddedProcess() {
2974
- const getHandles = process._getActiveHandles;
2975
- const handles = typeof getHandles === "function" ? getHandles() : void 0;
2976
- if (!Array.isArray(handles)) return;
2977
- for (const handle of handles) if (isSassEmbeddedChild(handle)) try {
2978
- handle.kill();
2979
- } catch {}
2980
- }
2981
2356
  //#endregion
2982
2357
  //#region src/cli/commands/analyze.ts
2983
2358
  function normalizeDisplayPath(value) {
@@ -3071,13 +2446,17 @@ function printAnalysisSummary(result) {
3071
2446
  if (duplicates.length > limit) logger_default.info(`- …其余 ${duplicates.length - limit} 项请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
3072
2447
  }
3073
2448
  function printBudgetCheckSummary(result) {
3074
- const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded");
2449
+ const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded" || item.status === "unknown");
3075
2450
  if (exceededItems.length === 0) {
3076
2451
  logger_default.success("包体预算检查通过");
3077
2452
  return false;
3078
2453
  }
3079
2454
  logger_default.error(`包体预算检查失败:${exceededItems.length} 项超限`);
3080
- 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
+ }
3081
2460
  return true;
3082
2461
  }
3083
2462
  function printGlassEaselCheckSummary(result) {
@@ -3253,107 +2632,111 @@ function printHmrProfileAnalysisSummary(result, configService) {
3253
2632
  }
3254
2633
  }
3255
2634
  function registerAnalyzeCommand(cli) {
3256
- 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) => {
3257
2636
  filterDuplicateOptions(options);
2637
+ options.uiHost = parseDashboardUiHost(options.uiHost);
3258
2638
  const configFile = resolveConfigFile(options);
3259
2639
  const outputJson = coerceBooleanOption(options.json);
3260
- const outputMarkdown = coerceBooleanOption(options.markdown);
3261
- const reportType = typeof options.report === "string" ? options.report.trim() : "";
3262
- const outputPrReport = reportType === "pr";
3263
- if (reportType && !outputPrReport) throw new Error(`不支持的 analyze report 类型:${reportType}`);
3264
- const budgetCheck = coerceBooleanOption(options.budgetCheck);
3265
- const glassEaselCheck = coerceBooleanOption(options.glassEaselCheck);
3266
- const launchDashboard = !outputJson && !outputMarkdown && !outputPrReport && !budgetCheck && !glassEaselCheck;
3267
- const targets = resolveRuntimeTargets(options);
3268
- const inlineConfig = createInlineConfig(targets);
3269
- let ctx;
3270
- try {
3271
- ctx = await createCompilerContext({
3272
- cwd: root,
3273
- mode: options.mode ?? "production",
3274
- configFile,
3275
- inlineConfig,
3276
- cliPlatform: targets.rawPlatform,
3277
- projectConfigPath: options.projectConfig
3278
- });
3279
- logRuntimeTarget(targets, {
3280
- silent: outputJson || outputMarkdown,
3281
- resolvedConfigPlatform: ctx.configService.platform
3282
- });
3283
- const outputOption = typeof options.output === "string" ? options.output.trim() : "";
3284
- if (options.hmrProfile !== void 0 && options.hmrProfile !== false) {
3285
- const profileOption = typeof options.hmrProfile === "string" && options.hmrProfile.trim() ? options.hmrProfile.trim() : ctx.configService.weappViteConfig.hmr?.profileJson;
3286
- const profilePath = resolveHmrProfileJsonPath({
3287
- cwd: ctx.configService.cwd,
3288
- option: profileOption,
3289
- 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
3290
2659
  });
3291
- if (!profilePath) throw new Error("未找到可用的 HMR profile 文件路径");
3292
- const hmrProfileResult = await analyzeHmrProfile({ profilePath });
3293
- const writtenPath = await writeAnalyzeResult(hmrProfileResult, outputOption, ctx.configService);
3294
- if (outputJson) {
3295
- if (!writtenPath) process.stdout.write(`${JSON.stringify(hmrProfileResult, null, 2)}\n`);
3296
- } else printHmrProfileAnalysisSummary(hmrProfileResult, ctx.configService);
3297
- return;
3298
- }
3299
- if (coerceBooleanOption(options.preload)) {
3300
- if (targets.kind !== "miniprogram" || ctx.configService.platform !== "weapp") throw new Error("preloadRule 分析目前仅支持微信小程序平台。");
3301
- const packageAnalysis = await analyzeSubpackages(ctx);
3302
- const preloadResult = await analyzePreloadRules(ctx, { packageAnalysis });
3303
- const writtenPath = await writeAnalyzeResult(preloadResult, outputOption, ctx.configService);
3304
- if (outputJson && !writtenPath) process.stdout.write(`${JSON.stringify(preloadResult, null, 2)}\n`);
3305
- if (!outputJson && !writtenPath) printPreloadAnalysisSummary(preloadResult);
3306
- return;
3307
- }
3308
- if (getBackendForCapability(targets, "web", "analyze")) {
3309
- const webResult = createWebAnalyzeResult(ctx.configService, { platform: "web" });
3310
- const writtenPath = await writeAnalyzeResult(webResult, outputOption, ctx.configService);
3311
- if (outputJson) {
3312
- if (!writtenPath) process.stdout.write(`${JSON.stringify(webResult, null, 2)}\n`);
3313
- } else printWebAnalysisSummary(webResult);
3314
- return;
3315
- }
3316
- if (!getBackendForCapability(targets, "miniprogram", "analyze")) {
3317
- logger_default.warn("当前命令不支持该平台,请通过 --platform weapp 或 --platform web 指定目标。");
3318
- return;
3319
- }
3320
- const previousResult = await readLatestAnalyzeHistorySnapshot(ctx.configService);
3321
- ctx.runtimeState.glassEasel.silent = Boolean(outputJson || outputMarkdown || outputPrReport || glassEaselCheck);
3322
- const artifactSnapshot = launchDashboard ? createDashboardArtifactSnapshot() : void 0;
3323
- const result = artifactSnapshot ? await analyzeSubpackages(ctx, { onArtifact: artifactSnapshot.capture }) : await analyzeSubpackages(ctx);
3324
- await writeAnalyzeHistorySnapshot(result, ctx.configService);
3325
- const writtenPath = await writeAnalyzeResult(result, outputOption, ctx.configService, outputPrReport ? "pr" : outputMarkdown ? "markdown" : "json", previousResult);
3326
- if (outputPrReport) {
3327
- if (!writtenPath) process.stdout.write(`${createAnalyzePrMarkdownReport(result, previousResult)}\n`);
3328
- } else if (outputMarkdown) {
3329
- if (!writtenPath) process.stdout.write(`${createAnalyzeMarkdownReport(result, previousResult)}\n`);
3330
- } else if (outputJson) {
3331
- if (!writtenPath) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
3332
- }
3333
- const budgetFailed = budgetCheck ? printBudgetCheckSummary(result) : false;
3334
- const glassEaselFailed = glassEaselCheck && !outputJson ? printGlassEaselCheckSummary(result) : glassEaselCheck && result.glassEasel.summary.errors > 0;
3335
- if (budgetFailed || glassEaselFailed) process.exitCode = 1;
3336
- if (budgetCheck || glassEaselCheck) return;
3337
- if (artifactSnapshot) {
3338
- printAnalysisSummary(result);
3339
- await startAnalyzeDashboard(result, {
3340
- artifacts: artifactSnapshot.files,
3341
- cwd: ctx.configService.cwd,
3342
- packageManagerAgent: ctx.configService.packageManager.agent,
3343
- pluginRoot: ctx.configService.absolutePluginRoot,
3344
- srcRoot: ctx.configService.absoluteSrcRoot,
3345
- previousResult
2660
+ logRuntimeTarget(targets, {
2661
+ silent: outputJson || outputMarkdown,
2662
+ resolvedConfigPlatform: ctx.configService.platform
3346
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();
3347
2738
  }
3348
- } catch (error) {
3349
- logger_default.error(error);
3350
- process.exitCode = 1;
3351
- } finally {
3352
- if (ctx) {
3353
- for (const backend of [...targets.entries].reverse()) if (backend.descriptor.capabilities.analyze) await backend.driver?.close?.(ctx);
3354
- }
3355
- terminateStaleSassEmbeddedProcess();
3356
- }
2739
+ });
3357
2740
  });
3358
2741
  }
3359
2742
  //#endregion
@@ -3453,6 +2836,631 @@ function setCommandNodeEnv(nodeEnv) {
3453
2836
  process.env.NODE_ENV = nodeEnv;
3454
2837
  }
3455
2838
  //#endregion
2839
+ //#region src/cli/upload/autoMetadata.ts
2840
+ /** 只读取命令根目录的清单,不向父目录查找或修改文件。 */
2841
+ async function readPackageVersion(packagePath) {
2842
+ let manifest;
2843
+ try {
2844
+ manifest = JSON.parse(await readFile(packagePath, "utf8"));
2845
+ } catch (cause) {
2846
+ throw new Error("--bump 无法读取命令根目录的 package.json。", { cause });
2847
+ }
2848
+ if (manifest === null || typeof manifest !== "object" || !("version" in manifest) || typeof manifest.version !== "string") throw new Error("--bump 要求命令根目录 package.json 的 version 为有效的 SemVer 字符串。");
2849
+ return manifest.version;
2850
+ }
2851
+ /** 在首次编译配置求值前准备一次元数据;预演不写文件,实际版本由 npm 同步。 */
2852
+ function validateAutoUploadMetadata(options, action) {
2853
+ const { bump, gitDesc } = options;
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;
2890
+ if (bump === void 0 && !gitDesc) return options;
2891
+ if (bump !== void 0 && options.uv !== void 0) throw new Error("--bump 不能与 --uv 同时使用。");
2892
+ if (gitDesc && options.desc !== void 0) throw new Error("--git-desc 不能与 --desc 同时使用。");
2893
+ if (bump !== void 0 && bump !== "patch" && bump !== "minor" && bump !== "major") throw new Error("--bump 仅支持 patch、minor 或 major。");
2894
+ const root = path$1.resolve(cwd);
2895
+ const packagePath = path$1.join(root, "package.json");
2896
+ let currentVersion;
2897
+ let nextVersion;
2898
+ if (bump !== void 0) {
2899
+ currentVersion = await readPackageVersion(packagePath);
2900
+ nextVersion = semverInc(currentVersion, bump) ?? void 0;
2901
+ if (!nextVersion) throw new Error("--bump 要求命令根目录 package.json 的 version 为有效的 SemVer 字符串。");
2902
+ }
2903
+ let desc;
2904
+ if (gitDesc) {
2905
+ try {
2906
+ desc = (await x("git", [
2907
+ "log",
2908
+ "-1",
2909
+ "--format=%s"
2910
+ ], {
2911
+ nodePath: false,
2912
+ throwOnError: true,
2913
+ nodeOptions: { cwd: root }
2914
+ })).stdout.trim();
2915
+ } catch (cause) {
2916
+ throw new Error("--git-desc 无法读取最新 Git 提交主题,请确认 Git 可用且仓库已有提交。", { cause });
2917
+ }
2918
+ if (!desc) throw new Error("--git-desc 要求最新 Git 提交主题非空。");
2919
+ }
2920
+ if (nextVersion) {
2921
+ if (options.dryRun) logger_default.info(`[upload] dry-run:计划版本 ${currentVersion} → ${nextVersion},未修改 package.json 或锁文件。`);
2922
+ else {
2923
+ try {
2924
+ await x("npm", [
2925
+ "version",
2926
+ nextVersion,
2927
+ "--no-git-tag-version",
2928
+ "--ignore-scripts",
2929
+ "--workspaces=false",
2930
+ "--prefix",
2931
+ root
2932
+ ], {
2933
+ nodePath: false,
2934
+ throwOnError: true,
2935
+ nodeOptions: { cwd: root }
2936
+ });
2937
+ } catch (cause) {
2938
+ throw new Error("--bump 执行 npm version 失败,请检查本地版本与锁文件后重试。", { cause });
2939
+ }
2940
+ if (await readPackageVersion(packagePath) !== nextVersion) throw new Error("--bump 执行 npm version 后,命令根目录的 package.json 版本未按预期更新。");
2941
+ logger_default.info(`[upload] 版本已更新:${currentVersion} → ${nextVersion}。后续构建或上传失败不会回退;重试同一版本时请移除 --bump。`);
2942
+ }
2943
+ }
2944
+ if (gitDesc) logger_default.info("[upload] 使用最新 Git 提交主题作为上传说明。");
2945
+ const metadata = { ...options };
2946
+ if (nextVersion !== void 0) metadata.uv = nextVersion;
2947
+ if (desc !== void 0) metadata.desc = desc;
2948
+ return metadata;
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
+ }
2959
+ //#endregion
2960
+ //#region src/cli/upload/env.ts
2961
+ /** 凭据不经过 Vite 的 env 调试日志;文件优先级和进程环境覆盖规则保持一致。 */
2962
+ async function loadUploadEnv(cwd, mode, root, envDir) {
2963
+ if (envDir === false) return { ...process.env };
2964
+ if (mode === "local") throw new Error("local 不能作为 mode,因为它与 .env.local 文件后缀冲突。");
2965
+ const directory = path$1.resolve(cwd, root ?? ".", envDir ?? ".");
2966
+ const parsed = {};
2967
+ for (const file of [
2968
+ ".env",
2969
+ ".env.local",
2970
+ `.env.${mode}`,
2971
+ `.env.${mode}.local`
2972
+ ]) {
2973
+ const content = await readFile(path$1.join(directory, file), "utf8").catch((error) => {
2974
+ if (error.code === "ENOENT") return "";
2975
+ throw error;
2976
+ });
2977
+ for (const [key, value] of Object.entries(parse$2(content))) {
2978
+ delete parsed[key];
2979
+ parsed[key] = value;
2980
+ }
2981
+ }
2982
+ const processEnv = { ...parsed };
2983
+ for (const [key, value] of Object.entries(process.env)) if (value !== void 0) {
2984
+ processEnv[key] = value;
2985
+ delete parsed[key];
2986
+ }
2987
+ expand({
2988
+ parsed,
2989
+ processEnv
2990
+ });
2991
+ return {
2992
+ ...parsed,
2993
+ ...process.env
2994
+ };
2995
+ }
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
3012
+ //#region src/cli/upload/result.ts
3013
+ /** 只接受可展示的预览结果,文件必须由本次任务生成,不能沿用旧二维码。 */
3014
+ async function validatePreviewResult(value, context) {
3015
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("预览工具未返回二维码或预览链接。");
3016
+ const input = value;
3017
+ const result = {};
3018
+ for (const key of ["qrCodeUrl", "previewUrl"]) {
3019
+ if (input[key] === void 0) continue;
3020
+ if (typeof input[key] !== "string" || !input[key].trim()) throw new Error("预览工具返回了无效链接。");
3021
+ let url;
3022
+ try {
3023
+ url = new URL(input[key]);
3024
+ } catch {
3025
+ throw new Error("预览工具返回了无效链接。");
3026
+ }
3027
+ if (url.username || url.password || key === "qrCodeUrl" && url.protocol !== "http:" && url.protocol !== "https:" || [
3028
+ "javascript:",
3029
+ "data:",
3030
+ "file:"
3031
+ ].includes(url.protocol)) throw new Error("预览工具返回了不支持的链接类型。");
3032
+ result[key] = input[key];
3033
+ }
3034
+ if (input.qrCodeFile !== void 0) {
3035
+ if (typeof input.qrCodeFile !== "string" || !context.qrCodePath || path$1.resolve(input.qrCodeFile) !== path$1.resolve(context.qrCodePath)) throw new Error("预览工具返回的二维码文件不是本次任务的输出路径。");
3036
+ const file = await stat(input.qrCodeFile);
3037
+ if (!file.isFile() || file.size === 0) throw new Error("预览二维码文件不存在或为空。");
3038
+ result.qrCodeFile = input.qrCodeFile;
3039
+ }
3040
+ if (!result.qrCodeUrl && !result.previewUrl && !result.qrCodeFile) throw new Error("预览工具未返回二维码或预览链接。");
3041
+ return result;
3042
+ }
3043
+ //#endregion
3044
+ //#region src/cli/upload/process.ts
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
+ }
3102
+ let completed = false;
3103
+ let started = false;
3104
+ let settled = false;
3105
+ let result = {};
3106
+ const captured = {
3107
+ stdout: "",
3108
+ stderr: ""
3109
+ };
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)}`;
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 连接中断,未收到完成确认");
3172
+ };
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) {
3191
+ completed = true;
3192
+ result = "result" in message && message.result && typeof message.result === "object" ? message.result : {};
3193
+ }
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);
3215
+ process.off("SIGINT", cancel);
3216
+ process.off("SIGTERM", cancel);
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"));
3233
+ return;
3234
+ }
3235
+ resolve({
3236
+ result,
3237
+ output
3238
+ });
3239
+ });
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
+ }
3264
+ });
3265
+ if (output) logger_default.info(output);
3266
+ return action === "preview" ? validatePreviewResult(result, context) : result;
3267
+ }
3268
+ //#endregion
3269
+ //#region src/cli/upload/project.ts
3270
+ /** 确保官方工具读取的目录正是本次构建输出,拒绝误传旧目录或同名配置。 */
3271
+ async function validateUploadProject(options) {
3272
+ const { projectConfigFileName } = getProjectPlatformOptions(options.platform);
3273
+ if (options.sourceConfigPath && path$1.basename(options.sourceConfigPath) !== projectConfigFileName) throw new Error(`上传工具只读取 ${projectConfigFileName},请使用此标准文件名,避免读取其他项目配置。`);
3274
+ const configPath = path$1.join(options.projectPath, projectConfigFileName);
3275
+ const config = JSON.parse(await readFile(configPath, "utf8"));
3276
+ if (!config || typeof config !== "object" || Array.isArray(config)) throw new TypeError(`${projectConfigFileName} 必须是 JSON 对象。`);
3277
+ const rootKey = options.platform === "swan" ? "smartProgramRoot" : "miniprogramRoot";
3278
+ const root = config[rootKey];
3279
+ if (typeof root !== "string" || !root.trim()) throw new Error(`${projectConfigFileName} 缺少代码目录 ${rootKey}。`);
3280
+ const sdkRoot = path$1.resolve(options.projectPath, root);
3281
+ const [actualRoot, outputRoot] = await Promise.all([realpath(sdkRoot), realpath(options.outDir)]);
3282
+ if (actualRoot !== outputRoot) throw new Error(`${projectConfigFileName} 的代码目录与本次构建输出不一致,已阻止上传旧产物。请检查项目配置根目录与 build.outDir。`);
3283
+ await access(path$1.join(outputRoot, "app.json"));
3284
+ }
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
3343
+ //#region src/cli/upload/builtProject.ts
3344
+ /** 仅由显式上传或预览入口读取参数,不执行构建或调用平台工具。 */
3345
+ async function createUploadTarget(config, options, action) {
3346
+ if (config.weappLibConfig?.enabled || config.pluginOnly) throw new Error(`${action} 仅支持完整小程序项目,不支持组件库或独立插件构建。`);
3347
+ const platform = resolveUploadPlatforms(config.platform)[0];
3348
+ const uploadConfig = action === "upload" ? config.weappViteConfig.upload : void 0;
3349
+ const version = (options.uv ?? uploadConfig?.version ?? config.packageJson.version)?.trim() ?? "";
3350
+ if (action === "upload" && !version) throw new Error("请通过 --uv、weapp.upload.version 或项目 package.json 的 version 指定上传版本。");
3351
+ const desc = (options.desc ?? uploadConfig?.desc)?.trim() || `${config.packageJson.name ?? platform}${version ? `@${version}` : ""}`;
3352
+ const env = options.dryRun ? {} : await loadUploadEnv(config.cwd, config.mode, config.inlineConfig.root, config.inlineConfig.envDir);
3353
+ const { projectConfigFileName } = getProjectPlatformOptions(config.platform);
3354
+ const generatedProjectConfig = config.multiPlatform.projectConfigs !== void 0;
3355
+ const projectPath = generatedProjectConfig ? config.outDir : config.multiPlatform.enabled ? path$1.dirname(config.outDir) : path$1.dirname(config.projectConfigPath ?? path$1.join(config.cwd, projectConfigFileName));
3356
+ const appid = config.projectConfig.appid ?? config.projectConfig.appId;
3357
+ const target = {
3358
+ platform,
3359
+ sourceConfigPath: config.projectConfigPath,
3360
+ context: {
3361
+ cwd: config.cwd,
3362
+ projectPath,
3363
+ appid: typeof appid === "string" ? appid : void 0,
3364
+ version,
3365
+ desc,
3366
+ qrCodePath: action === "preview" && !options.dryRun ? path$1.join(config.cwd, ".weapp-vite", "preview", `${platform}-${randomUUID()}.png`) : void 0,
3367
+ env: {
3368
+ ...process.env,
3369
+ ...env
3370
+ }
3371
+ }
3372
+ };
3373
+ (config.inlineConfig.plugins ??= []).push({
3374
+ name: "weapp-vite:upload-output",
3375
+ apply: (viteConfig) => resolveWeappViteHostMeta(viteConfig)?.runtime === "miniprogram",
3376
+ writeBundle(output, bundle) {
3377
+ if (output.dir && Object.hasOwn(bundle, "app.json")) {
3378
+ target.outDir = path$1.resolve(output.dir);
3379
+ if (generatedProjectConfig) target.context.projectPath = target.outDir;
3380
+ }
3381
+ }
3382
+ });
3383
+ return target;
3384
+ }
3385
+ /** 只消费本次构建的产物;产物校验通过前不会准备凭据或调用平台工具。 */
3386
+ async function executeUploadTarget(target, options, action, execution = {}) {
3387
+ const { platform, context } = target;
3388
+ if (!target.outDir) throw new Error("本次构建未写出 app.json,已阻止上传或预览旧产物。");
3389
+ await validateUploadProject({
3390
+ platform,
3391
+ projectPath: context.projectPath,
3392
+ outDir: target.outDir,
3393
+ sourceConfigPath: target.sourceConfigPath
3394
+ });
3395
+ if (options.dryRun) {
3396
+ logger_default.success(`[${action}:${platform}] dry-run:构建完成,未校验凭据或调用平台工具。项目:${path$1.relative(context.cwd, context.projectPath) || "."}`);
3397
+ return;
3398
+ }
3399
+ const upload = await prepareUpload(platform, context, 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
+ });
3405
+ if (action === "preview") {
3406
+ if (!result) throw new Error("预览工具未返回二维码或预览链接。");
3407
+ for (const [label, value] of [
3408
+ ["二维码图片", result.qrCodeUrl],
3409
+ ["预览链接", result.previewUrl],
3410
+ ["二维码文件", result.qrCodeFile && path$1.relative(context.cwd, result.qrCodeFile)]
3411
+ ]) if (value) logger_default.info(`[preview:${platform}] ${label}:${redactUploadSecrets(value, upload.secrets)}`);
3412
+ logger_default.success(`[preview:${platform}] 预览已生成(未上传开发版本、未提审、未正式发布)。`);
3413
+ } else logger_default.success(`[upload:${platform}] ${context.version} 上传完成(未提审、未正式发布)。`);
3414
+ return result;
3415
+ }
3416
+ //#endregion
3417
+ //#region src/cli/upload/options.ts
3418
+ function readUploadMetadata(cli) {
3419
+ for (const name of [
3420
+ "uv",
3421
+ "desc",
3422
+ "bump"
3423
+ ]) {
3424
+ const value = cli.options[name];
3425
+ if (typeof value === "boolean" || Array.isArray(value) && value.some((item) => typeof item === "boolean")) throw new Error(`--${name} 需要指定字符串参数。`);
3426
+ }
3427
+ const { values } = parseArgs({
3428
+ args: cli.rawArgs.slice(2),
3429
+ allowPositionals: true,
3430
+ strict: false,
3431
+ options: {
3432
+ uv: { type: "string" },
3433
+ desc: { type: "string" },
3434
+ bump: { type: "string" }
3435
+ }
3436
+ });
3437
+ return {
3438
+ uv: typeof values.uv === "string" ? values.uv : void 0,
3439
+ desc: typeof values.desc === "string" ? values.desc : void 0,
3440
+ bump: typeof values.bump === "string" ? values.bump : void 0
3441
+ };
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
+ }
3451
+ /** 普通构建不读取上传元数据;上传必须是显式的一次性构建操作。 */
3452
+ function resolveBuildUploadOptions(cli, options) {
3453
+ if (options.upload !== true) {
3454
+ if (options.uv !== void 0 || options.desc !== void 0 || options.bump !== void 0 || options.gitDesc !== void 0 || options.dryRun !== void 0) throw new Error("--uv、--desc、--bump、--git-desc 和 --dry-run 仅能与 --upload 一起使用。");
3455
+ return;
3456
+ }
3457
+ if (options.watch) throw new Error("--upload 不能与 --watch 一起使用,请执行一次性构建。");
3458
+ return {
3459
+ ...options,
3460
+ ...readUploadMetadata(cli)
3461
+ };
3462
+ }
3463
+ //#endregion
3456
3464
  //#region src/cli/commands/build.ts
3457
3465
  function emitDashboardEvents$1(handle, events) {
3458
3466
  handle?.emitRuntimeEvents(events);
@@ -3488,22 +3496,36 @@ function scheduleCompletedProductionBuildExit(options, analyzeHandle) {
3488
3496
  }, 0).unref?.();
3489
3497
  }
3490
3498
  function registerBuildCommand(cli) {
3491
- 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`).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) => {
3492
3500
  let analyzeHandle;
3501
+ let session;
3493
3502
  let ctx;
3494
3503
  let targets;
3504
+ let uploadMetadataRollback;
3495
3505
  let buildCompleted = false;
3506
+ let buildFailed = false;
3496
3507
  try {
3508
+ options = { ...options };
3497
3509
  filterDuplicateOptions(options);
3510
+ options.uiHost = parseDashboardUiHost(options.uiHost);
3511
+ let uploadOptions = resolveBuildUploadOptions(cli, options);
3498
3512
  setCommandNodeEnv("production");
3499
3513
  const cwd = root ?? process.cwd();
3500
3514
  const configFile = resolveConfigFile(options);
3501
3515
  targets = resolveRuntimeTargets(options);
3516
+ if (uploadOptions) validateAutoUploadMetadata(uploadOptions, "upload");
3517
+ if (uploadOptions && !getBackendForCapability(targets, "miniprogram", "build")) throw new Error("--upload 仅支持包含小程序目标的构建,不能用于纯 Web 构建。");
3502
3518
  const inlineConfig = createInlineConfig(targets, {
3503
3519
  scope: options.scope,
3504
3520
  inlineConfig: createBuildInlineConfig(options)
3505
3521
  });
3506
- 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({
3507
3529
  cwd,
3508
3530
  mode: options.mode ?? "production",
3509
3531
  configFile,
@@ -3513,7 +3535,16 @@ function registerBuildCommand(cli) {
3513
3535
  emitDefaultAutoImportOutputs: false,
3514
3536
  preloadAppEntry: false
3515
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));
3516
3546
  const { configService } = ctx;
3547
+ const uploadTarget = uploadOptions ? await createUploadTarget(configService, uploadOptions, "upload") : void 0;
3517
3548
  const miniBackend = getBackendForCapability(targets, "miniprogram", "build");
3518
3549
  const webBackend = getBackendForCapability(targets, "web", "build");
3519
3550
  logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
@@ -3521,7 +3552,7 @@ function registerBuildCommand(cli) {
3521
3552
  for (const backend of targets.select("build")) {
3522
3553
  if (backend.descriptor.id === "miniprogram") {
3523
3554
  const miniBuildStartedAt = Date.now();
3524
- const output = await backend.driver.build(ctx, options);
3555
+ const output = await session.run(() => backend.driver.build(ctx, options));
3525
3556
  const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
3526
3557
  logger_default.success(`小程序构建完成,耗时:${colors.green(formatDuration(miniBuildDurationMs))}`);
3527
3558
  if (!Array.isArray(output) && "output" in output) logBuildPackageSizeReport({
@@ -3538,6 +3569,7 @@ function registerBuildCommand(cli) {
3538
3569
  const analyzeDurationMs = Date.now() - analyzeStartedAt;
3539
3570
  analyzeHandle = await startAnalyzeDashboard(analyzeResult, {
3540
3571
  watch: true,
3572
+ uiHost: options.uiHost,
3541
3573
  artifacts: artifactSnapshot.files,
3542
3574
  cwd: configService.cwd,
3543
3575
  packageManagerAgent: configService.packageManager.agent,
@@ -3567,7 +3599,7 @@ function registerBuildCommand(cli) {
3567
3599
  if (backend.descriptor.id !== "web" || !webConfig?.enabled) continue;
3568
3600
  const webBuildStartedAt = Date.now();
3569
3601
  try {
3570
- await backend.driver.build(ctx, options);
3602
+ await session.run(() => backend.driver.build(ctx, options));
3571
3603
  const webBuildDurationMs = Date.now() - webBuildStartedAt;
3572
3604
  logger_default.success(`Web 构建完成,输出目录:${colors.green(configService.relativeCwd(webConfig.outDir))},耗时:${colors.green(formatDuration(webBuildDurationMs))}`);
3573
3605
  emitDashboardEvents$1(analyzeHandle, [{
@@ -3591,6 +3623,7 @@ function registerBuildCommand(cli) {
3591
3623
  throw error;
3592
3624
  }
3593
3625
  }
3626
+ if (uploadTarget && uploadOptions) await executeUploadTarget(uploadTarget, uploadOptions, "upload");
3594
3627
  if (miniBackend) logBuildAppFinish(configService, void 0, { skipWeb: !webBackend });
3595
3628
  if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
3596
3629
  emitDashboardEvents$1(analyzeHandle, [{
@@ -3600,7 +3633,8 @@ function registerBuildCommand(cli) {
3600
3633
  detail: "构建完成后准备打开 IDE 项目。",
3601
3634
  tags: ["ide", "open"]
3602
3635
  }]);
3603
- await openIde(configService.platform, resolveIdeProjectPath(configService.mpDistRoot), {
3636
+ const generatedProjectConfig = configService.multiPlatform.projectConfigs !== void 0;
3637
+ await openIde(configService.platform, resolveIdeProjectPath(generatedProjectConfig ? configService.outDir : configService.mpDistRoot, generatedProjectConfig), {
3604
3638
  openRecovery: options.openRecovery,
3605
3639
  trustProject: options.trustProject,
3606
3640
  openStrategy: options.ideOpenStrategy
@@ -3608,9 +3642,12 @@ function registerBuildCommand(cli) {
3608
3642
  }
3609
3643
  if (analyzeHandle) await analyzeHandle.waitForExit();
3610
3644
  buildCompleted = true;
3645
+ } catch (error) {
3646
+ buildFailed = true;
3647
+ throw error;
3611
3648
  } finally {
3612
- if (ctx && targets) for (const backend of [...targets.select("build")].reverse()) await backend.driver.close(ctx);
3613
- terminateStaleSassEmbeddedProcess();
3649
+ if (buildFailed) await session?.close().catch((error) => logger_default.error(error));
3650
+ else await session?.close();
3614
3651
  if (buildCompleted) scheduleCompletedProductionBuildExit(options, analyzeHandle);
3615
3652
  }
3616
3653
  });
@@ -3623,6 +3660,39 @@ function registerCloseCommand(cli) {
3623
3660
  });
3624
3661
  }
3625
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
3626
3696
  //#region src/schematics.ts
3627
3697
  function composePath(outDir, filename) {
3628
3698
  return `${outDir}${outDir ? "/" : ""}${filename}`;
@@ -3707,60 +3777,6 @@ async function generate(options) {
3707
3777
  }
3708
3778
  }
3709
3779
  //#endregion
3710
- //#region src/cli/loadConfig.ts
3711
- async function loadConfig(configFile) {
3712
- const cwd = process.cwd();
3713
- let resolvedConfigFile = configFile;
3714
- if (resolvedConfigFile && !path.isAbsolute(resolvedConfigFile)) resolvedConfigFile = path.resolve(cwd, resolvedConfigFile);
3715
- const configEnv = {
3716
- command: "serve",
3717
- mode: "development"
3718
- };
3719
- let loaded;
3720
- try {
3721
- loaded = await loadViteConfigFile(configEnv, resolvedConfigFile, cwd, void 0, void 0, "runner");
3722
- } catch (error) {
3723
- const cjsError = createCjsConfigLoadError({
3724
- error,
3725
- configPath: resolvedConfigFile,
3726
- cwd
3727
- });
3728
- if (cjsError) throw cjsError;
3729
- throw error;
3730
- }
3731
- const weappConfigFilePath = await resolveWeappConfigFile({
3732
- root: cwd,
3733
- specified: resolvedConfigFile
3734
- });
3735
- let weappLoaded;
3736
- if (weappConfigFilePath) {
3737
- const normalizedWeappPath = path.resolve(weappConfigFilePath);
3738
- const normalizedLoadedPath = loaded?.path ? path.resolve(loaded.path) : void 0;
3739
- if (normalizedLoadedPath && normalizedLoadedPath === normalizedWeappPath) weappLoaded = loaded;
3740
- else try {
3741
- weappLoaded = await loadViteConfigFile(configEnv, weappConfigFilePath, cwd, void 0, void 0, "runner");
3742
- } catch (error) {
3743
- const cjsError = createCjsConfigLoadError({
3744
- error,
3745
- configPath: weappConfigFilePath,
3746
- cwd
3747
- });
3748
- if (cjsError) throw cjsError;
3749
- throw error;
3750
- }
3751
- }
3752
- if (!loaded && !weappLoaded) return;
3753
- const config = weappLoaded?.config ? defu(weappLoaded.config, loaded?.config ?? {}) : loaded?.config ?? {};
3754
- const dependencySet = /* @__PURE__ */ new Set();
3755
- for (const dependency of loaded?.dependencies ?? []) dependencySet.add(dependency);
3756
- for (const dependency of weappLoaded?.dependencies ?? []) dependencySet.add(dependency);
3757
- return {
3758
- config,
3759
- path: weappLoaded?.path ?? loaded?.path ?? resolvedConfigFile,
3760
- dependencies: Array.from(dependencySet)
3761
- };
3762
- }
3763
- //#endregion
3764
3780
  //#region src/cli/commands/generate.ts
3765
3781
  function registerGenerateCommand(cli) {
3766
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) => {
@@ -3800,6 +3816,7 @@ const DEFAULT_FORWARD_CONSOLE_LEVELS = [
3800
3816
  let activeForwardConsoleSession;
3801
3817
  let activeForwardConsoleBridgeOptions;
3802
3818
  let activeForwardConsoleStart;
3819
+ let activeForwardConsoleClose;
3803
3820
  let forwardConsoleLifecycle = 0;
3804
3821
  const FORWARD_CONSOLE_RETRY_DELAY_MS = 1e3;
3805
3822
  const FORWARD_CONSOLE_RETRY_TIMES = 5;
@@ -3945,15 +3962,9 @@ async function pauseActiveForwardConsole() {
3945
3962
  if (activeForwardConsoleStart) return await activeForwardConsoleStart;
3946
3963
  if (activeForwardConsoleSession) return true;
3947
3964
  const startTask = (async () => {
3965
+ let resumedSession;
3948
3966
  try {
3949
- const resumedSession = await startForwardConsoleBridge(bridgeOptions);
3950
- if (lifecycle !== forwardConsoleLifecycle) {
3951
- await resumedSession.close();
3952
- return false;
3953
- }
3954
- activeForwardConsoleSession = resumedSession;
3955
- activeForwardConsoleBridgeOptions = bridgeOptions;
3956
- return true;
3967
+ resumedSession = await startForwardConsoleBridge(bridgeOptions);
3957
3968
  } catch (error) {
3958
3969
  if (lifecycle === forwardConsoleLifecycle) {
3959
3970
  const message = error instanceof Error ? error.message : String(error);
@@ -3961,6 +3972,13 @@ async function pauseActiveForwardConsole() {
3961
3972
  }
3962
3973
  return false;
3963
3974
  }
3975
+ if (lifecycle !== forwardConsoleLifecycle) {
3976
+ await resumedSession.close();
3977
+ return false;
3978
+ }
3979
+ activeForwardConsoleSession = resumedSession;
3980
+ activeForwardConsoleBridgeOptions = bridgeOptions;
3981
+ return true;
3964
3982
  })();
3965
3983
  activeForwardConsoleStart = startTask;
3966
3984
  try {
@@ -3973,21 +3991,32 @@ async function pauseActiveForwardConsole() {
3973
3991
  /**
3974
3992
  * @description 关闭当前 DevTools 日志桥并释放共享 automator 会话。
3975
3993
  */
3976
- async function closeActiveForwardConsole() {
3994
+ function closeActiveForwardConsole() {
3995
+ if (activeForwardConsoleClose) return activeForwardConsoleClose;
3977
3996
  forwardConsoleLifecycle += 1;
3978
3997
  const session = activeForwardConsoleSession;
3979
3998
  const pendingStart = activeForwardConsoleStart;
3980
3999
  activeForwardConsoleSession = void 0;
3981
4000
  activeForwardConsoleBridgeOptions = void 0;
3982
- await session?.close();
3983
- 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;
3984
4011
  }
3985
4012
  /**
3986
4013
  * @description 在 weapp 开发态按需启动控制台转发。
3987
4014
  */
3988
4015
  async function maybeStartForwardConsole(options) {
3989
- if (options.platform !== "weapp") return false;
3990
- const projectPath = resolveIdeProjectPath(options.mpDistRoot) ?? options.cwd;
4016
+ if (options.platform !== "weapp" || activeForwardConsoleClose) return false;
4017
+ const multiPlatform = options.weappViteConfig?.multiPlatform;
4018
+ const generatedProjectConfig = typeof multiPlatform === "object" && multiPlatform.projectConfigs !== void 0;
4019
+ const projectPath = resolveIdeProjectRoot(options.mpDistRoot, options.cwd, generatedProjectConfig);
3991
4020
  if (!projectPath) return false;
3992
4021
  if (activeForwardConsoleSession) return true;
3993
4022
  if (activeForwardConsoleStart) return await activeForwardConsoleStart;
@@ -4026,7 +4055,7 @@ async function maybeStartForwardConsole(options) {
4026
4055
  openedOnly: true,
4027
4056
  preferOpenedSession: true
4028
4057
  };
4029
- return await activateSession(await startForwardConsoleBridge(recoveredOptions), recoveredOptions);
4058
+ return activateSession(await startForwardConsoleBridge(recoveredOptions), recoveredOptions);
4030
4059
  } catch (recoveryError) {
4031
4060
  if (lifecycle !== forwardConsoleLifecycle) return false;
4032
4061
  const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
@@ -4035,7 +4064,7 @@ async function maybeStartForwardConsole(options) {
4035
4064
  }
4036
4065
  };
4037
4066
  try {
4038
- return await activateSession(await startForwardConsoleBridge(bridgeOptions), bridgeOptions);
4067
+ return activateSession(await startForwardConsoleBridge(bridgeOptions), bridgeOptions);
4039
4068
  } catch (error) {
4040
4069
  if (lifecycle !== forwardConsoleLifecycle) return false;
4041
4070
  if (options.recoverAutomatorSession && isRecoverableAutomatorStartError(error)) return await recoverAutomatorSession();
@@ -4051,7 +4080,7 @@ async function maybeStartForwardConsole(options) {
4051
4080
  preferOpenedSession: true
4052
4081
  };
4053
4082
  try {
4054
- return await activateSession(await startForwardConsoleBridge(fallbackOptions), fallbackOptions);
4083
+ return activateSession(await startForwardConsoleBridge(fallbackOptions), fallbackOptions);
4055
4084
  } catch (fallbackError) {
4056
4085
  if (lifecycle !== forwardConsoleLifecycle) return false;
4057
4086
  const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
@@ -4072,19 +4101,6 @@ async function maybeStartForwardConsole(options) {
4072
4101
  function isFiniteNumber(value) {
4073
4102
  return typeof value === "number" && Number.isFinite(value);
4074
4103
  }
4075
- function parseLatestHmrProfileSample(content) {
4076
- const lines = content.split(/\r?\n/);
4077
- for (let index = lines.length - 1; index >= 0; index -= 1) {
4078
- const trimmed = lines[index]?.trim();
4079
- if (!trimmed) continue;
4080
- try {
4081
- const parsed = JSON.parse(trimmed);
4082
- if (isFiniteNumber(parsed.totalMs)) return parsed;
4083
- } catch {
4084
- continue;
4085
- }
4086
- }
4087
- }
4088
4104
  function formatPhaseHint(sample) {
4089
4105
  const topPhase = [
4090
4106
  {
@@ -4274,7 +4290,8 @@ async function readLatestHmrProfileSummary(options) {
4274
4290
  if (!profilePath) return;
4275
4291
  const content = await fs$1.readFile(profilePath, "utf8").catch(() => void 0);
4276
4292
  if (!content) return;
4277
- const sample = parseLatestHmrProfileSample(content);
4293
+ const { samples, skippedLineCount } = readHmrProfileLines(content);
4294
+ const sample = samples.at(-1);
4278
4295
  if (!sample || !isFiniteNumber(sample.totalMs)) return;
4279
4296
  const relativeCwd = options.relativeCwd ?? ((value) => value);
4280
4297
  const segments = [`[hmr] 最近一次热更新 ${sample.totalMs.toFixed(2)} ms`];
@@ -4282,7 +4299,8 @@ async function readLatestHmrProfileSummary(options) {
4282
4299
  if (sample.file) segments.push(relativeCwd(sample.file));
4283
4300
  const phaseHint = formatPhaseHint(sample);
4284
4301
  if (phaseHint) segments.push(`主耗时 ${phaseHint}`);
4285
- 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} 条不兼容、未完成或无效记录`);
4286
4304
  return {
4287
4305
  file: sample.file,
4288
4306
  profilePath,
@@ -4335,20 +4353,19 @@ async function runIdeDoctor(projectPath, options) {
4335
4353
  status: "unknown"
4336
4354
  };
4337
4355
  if (resolvedCli.cliPath) try {
4338
- await isWechatIdeLoggedIn({
4339
- nonInteractive: true,
4340
- silent: true
4341
- });
4342
- loginCheck = {
4343
- status: "ok",
4344
- 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}),不能据此认定未登录。`
4345
4364
  };
4346
- } catch (error) {
4365
+ } catch {
4347
4366
  loginCheck = {
4348
- fix: "打开微信开发者工具完成登录后重试,或执行 `wv ide doctor --json` 查看 CLI 错误。",
4349
- message: error instanceof Error ? error.message : String(error),
4350
- status: "warning",
4351
- value: false
4367
+ message: "登录查询执行失败,登录状态仍未确认。",
4368
+ status: "unknown"
4352
4369
  };
4353
4370
  }
4354
4371
  const projectCheck = projectPath ? {
@@ -4377,11 +4394,20 @@ async function runIdeDoctor(projectPath, options) {
4377
4394
  status: "ok",
4378
4395
  value: true
4379
4396
  };
4380
- if (typeof miniProgram.toolInfo === "function") toolCheck = {
4381
- status: "ok",
4382
- value: await miniProgram.toolInfo()
4383
- };
4384
- 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
+ }
4385
4411
  } catch (error) {
4386
4412
  automatorCheck = {
4387
4413
  fix: "先执行 `wv open` 打开项目;若服务端口已开启,关闭多余 DevTools 窗口后重试。",
@@ -4530,7 +4556,7 @@ async function runIdeCommand(action, root, options) {
4530
4556
  * @description 注册 IDE 相关子命令。
4531
4557
  */
4532
4558
  function registerIdeCommand(cli) {
4533
- 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) => {
4534
4560
  await runIdeCommand(action, root, options);
4535
4561
  });
4536
4562
  }
@@ -4546,405 +4572,6 @@ function registerInitCommand(cli) {
4546
4572
  });
4547
4573
  }
4548
4574
  //#endregion
4549
- //#region src/cli/mcpClient/shared.ts
4550
- const CODEX_BLOCK_PREFIX = "# >>> weapp-vite mcp ";
4551
- const CODEX_BLOCK_SUFFIX = " >>>";
4552
- const CODEX_BLOCK_END_PREFIX = "# <<< weapp-vite mcp ";
4553
- const CODEX_BLOCK_END_SUFFIX = " <<<";
4554
- const DEFAULT_HTTP_TIMEOUT_MS = 1500;
4555
- const WORKSPACE_FOLDER_TOKEN = "${workspaceFolder}";
4556
- const REG_CodexUrl = /^\s*url\s*=\s*"([^"]+)"/m;
4557
- const REG_CodexCommand = /^\s*command\s*=\s*"([^"]+)"/m;
4558
- const REG_CodexArgs = /^\s*args\s*=\s*\[([^\]]*)\]/m;
4559
- const REG_FirstTomlString = /"([^"]+)"/;
4560
- function resolveTargetWorkspaceRoot(target) {
4561
- if (target.client === "cursor") return path.dirname(path.dirname(target.configPath));
4562
- if (target.client === "claude-code") return path.dirname(target.configPath);
4563
- return process.cwd();
4564
- }
4565
- function escapeRegExp(value) {
4566
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4567
- }
4568
- function renderTomlString(value) {
4569
- return JSON.stringify(value);
4570
- }
4571
- function sanitizeServerNameSegment(value) {
4572
- return value.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
4573
- }
4574
- function resolveServerName(workspaceRoot) {
4575
- return `weapp-vite-${sanitizeServerNameSegment(path.basename(workspaceRoot)) || "project"}`;
4576
- }
4577
- function resolveNodeBinCommand() {
4578
- return process.execPath || "node";
4579
- }
4580
- function resolveProjectCliBin(workspaceRoot) {
4581
- return path.join(workspaceRoot, "node_modules", "weapp-vite", "bin", "weapp-vite.js");
4582
- }
4583
- function renderCommandArgs(args) {
4584
- return args.map(renderTomlString).join(", ");
4585
- }
4586
- function renderCodexBlock(serverName, entry) {
4587
- const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
4588
- const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
4589
- if (entry.url) return `${startMarker}
4590
- [mcp_servers.${serverName}]
4591
- url = ${renderTomlString(entry.url)}
4592
- ${endMarker}
4593
- `;
4594
- return `${startMarker}
4595
- [mcp_servers.${serverName}]
4596
- command = ${renderTomlString(entry.command ?? resolveNodeBinCommand())}
4597
- args = [${renderCommandArgs(entry.args ?? [])}]
4598
- ${endMarker}
4599
- `;
4600
- }
4601
- function renderJsonPreview(serverName, entry) {
4602
- return `${JSON.stringify({ mcpServers: { [serverName]: entry } }, null, 2)}
4603
- `;
4604
- }
4605
- function resolveCommandEntry(client, workspaceRoot) {
4606
- const binPath = resolveProjectCliBin(workspaceRoot);
4607
- if (client === "cursor") return {
4608
- command: "node",
4609
- args: [
4610
- `${WORKSPACE_FOLDER_TOKEN}/node_modules/weapp-vite/bin/weapp-vite.js`,
4611
- "mcp",
4612
- "--workspace-root",
4613
- WORKSPACE_FOLDER_TOKEN
4614
- ]
4615
- };
4616
- return {
4617
- command: resolveNodeBinCommand(),
4618
- args: [
4619
- binPath,
4620
- "mcp",
4621
- "--workspace-root",
4622
- workspaceRoot
4623
- ]
4624
- };
4625
- }
4626
- function resolveHttpEntry(client, url) {
4627
- if (client === "claude-code") return {
4628
- type: "http",
4629
- url
4630
- };
4631
- return { url };
4632
- }
4633
- function resolveConfigEntry(client, transport, workspaceRoot, url) {
4634
- return transport === "http" ? resolveHttpEntry(client, url ?? "") : resolveCommandEntry(client, workspaceRoot);
4635
- }
4636
- function resolveCodexBlockPattern(serverName) {
4637
- const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
4638
- const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
4639
- return new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
4640
- }
4641
- function upsertCodexManagedBlock(content, serverName, block) {
4642
- const withoutExisting = content.trimEnd().replace(resolveCodexBlockPattern(serverName), "").trimEnd();
4643
- if (!withoutExisting) return block;
4644
- return `${withoutExisting}\n\n${block}`;
4645
- }
4646
- function parseJsonConfig(content, configPath) {
4647
- if (!content.trim()) return {};
4648
- try {
4649
- return JSON.parse(content);
4650
- } catch (error) {
4651
- const message = error instanceof Error ? error.message : String(error);
4652
- throw new Error(`无法解析 MCP 配置文件 ${configPath}:${message}`);
4653
- }
4654
- }
4655
- function resolveTarget(client, workspaceRoot) {
4656
- const serverName = resolveServerName(workspaceRoot);
4657
- if (client === "codex") return {
4658
- client,
4659
- configPath: path.join(os.homedir(), ".codex", "config.toml"),
4660
- displayName: "Codex",
4661
- serverName
4662
- };
4663
- if (client === "claude-code") return {
4664
- client,
4665
- configPath: path.join(workspaceRoot, ".mcp.json"),
4666
- displayName: "Claude Code",
4667
- serverName
4668
- };
4669
- return {
4670
- client,
4671
- configPath: path.join(workspaceRoot, ".cursor", "mcp.json"),
4672
- displayName: "Cursor",
4673
- serverName
4674
- };
4675
- }
4676
- //#endregion
4677
- //#region src/cli/mcpClient/inspect.ts
4678
- async function probeHttpEndpoint(url) {
4679
- const controller = new AbortController();
4680
- const timer = setTimeout$1(() => {
4681
- controller.abort();
4682
- }, DEFAULT_HTTP_TIMEOUT_MS);
4683
- try {
4684
- return (await fetch(url, {
4685
- method: "GET",
4686
- signal: controller.signal
4687
- })).status > 0;
4688
- } catch {
4689
- return false;
4690
- } finally {
4691
- clearTimeout$1(timer);
4692
- controller.abort();
4693
- }
4694
- }
4695
- async function inspectJsonConfig(target) {
4696
- if (!await fs$1.pathExists(target.configPath)) return {
4697
- configExists: false,
4698
- configPath: target.configPath,
4699
- displayName: target.displayName,
4700
- issues: [`未找到配置文件:${target.configPath}`],
4701
- serverName: target.serverName
4702
- };
4703
- const entry = parseJsonConfig(await fs$1.readFile(target.configPath, "utf8"), target.configPath).mcpServers?.[target.serverName];
4704
- if (!entry) return {
4705
- configExists: true,
4706
- configPath: target.configPath,
4707
- displayName: target.displayName,
4708
- issues: [`配置文件中未找到服务器 ${target.serverName}`],
4709
- serverName: target.serverName
4710
- };
4711
- const transport = entry.url ? "http" : "command";
4712
- const issues = [];
4713
- let httpReachable;
4714
- if (transport === "command") {
4715
- const normalizedBin = (entry.args?.[0])?.replaceAll(WORKSPACE_FOLDER_TOKEN, resolveTargetWorkspaceRoot(target));
4716
- if (!normalizedBin || !await fs$1.pathExists(normalizedBin)) issues.push(`未找到本地 CLI 入口:${normalizedBin ?? "缺少 args[0]"}`);
4717
- } else if (entry.url) {
4718
- httpReachable = await probeHttpEndpoint(entry.url);
4719
- if (!httpReachable) issues.push(`HTTP MCP 地址不可达:${entry.url}`);
4720
- }
4721
- return {
4722
- configExists: true,
4723
- configPath: target.configPath,
4724
- displayName: target.displayName,
4725
- httpReachable,
4726
- issues,
4727
- serverName: target.serverName,
4728
- transport
4729
- };
4730
- }
4731
- async function inspectCodexConfig(target) {
4732
- if (!await fs$1.pathExists(target.configPath)) return {
4733
- configExists: false,
4734
- configPath: target.configPath,
4735
- displayName: target.displayName,
4736
- issues: [`未找到配置文件:${target.configPath}`],
4737
- serverName: target.serverName
4738
- };
4739
- const blockMatch = (await fs$1.readFile(target.configPath, "utf8")).match(resolveCodexBlockPattern(target.serverName));
4740
- if (!blockMatch?.[0]) return {
4741
- configExists: true,
4742
- configPath: target.configPath,
4743
- displayName: target.displayName,
4744
- issues: [`未找到 weapp-vite 生成的 Codex 配置区块:${target.serverName}`],
4745
- serverName: target.serverName
4746
- };
4747
- const block = blockMatch[0];
4748
- const issues = [];
4749
- const urlMatch = block.match(REG_CodexUrl);
4750
- const commandMatch = block.match(REG_CodexCommand);
4751
- const argsMatch = block.match(REG_CodexArgs);
4752
- if (urlMatch?.[1]) {
4753
- const reachable = await probeHttpEndpoint(urlMatch[1]);
4754
- if (!reachable) issues.push(`HTTP MCP 地址不可达:${urlMatch[1]}`);
4755
- return {
4756
- configExists: true,
4757
- configPath: target.configPath,
4758
- displayName: target.displayName,
4759
- httpReachable: reachable,
4760
- issues,
4761
- serverName: target.serverName,
4762
- transport: "http"
4763
- };
4764
- }
4765
- const binPath = (argsMatch?.[1]?.match(REG_FirstTomlString))?.[1];
4766
- if (!commandMatch?.[1]) issues.push("Codex 配置区块缺少 command 字段");
4767
- if (!binPath || !await fs$1.pathExists(binPath)) issues.push(`未找到本地 CLI 入口:${binPath ?? "缺少 args[0]"}`);
4768
- return {
4769
- configExists: true,
4770
- configPath: target.configPath,
4771
- displayName: target.displayName,
4772
- issues,
4773
- serverName: target.serverName,
4774
- transport: "command"
4775
- };
4776
- }
4777
- async function inspectMcpClientConfig(options) {
4778
- const target = resolveTarget(options.client, path.resolve(options.workspaceRoot));
4779
- if (target.client === "codex") return await inspectCodexConfig(target);
4780
- return await inspectJsonConfig(target);
4781
- }
4782
- //#endregion
4783
- //#region src/cli/mcpClient/plan.ts
4784
- function resolveSupportedMcpClient(input) {
4785
- if (input === "codex" || input === "claude-code" || input === "cursor") return input;
4786
- throw new Error(`不支持的 MCP 客户端:${input}。当前仅支持 codex、claude-code、cursor。`);
4787
- }
4788
- function buildMcpClientConfigPlan(options) {
4789
- const workspaceRoot = path.resolve(options.workspaceRoot);
4790
- const target = resolveTarget(options.client, workspaceRoot);
4791
- const entry = resolveConfigEntry(options.client, options.transport, workspaceRoot, options.url);
4792
- if (options.transport === "http" && !entry.url) throw new Error("HTTP 模式缺少 MCP 服务地址,请使用 --url 指定或在当前项目中检测到可用地址。");
4793
- return {
4794
- entry,
4795
- preview: target.client === "codex" ? renderCodexBlock(target.serverName, entry) : renderJsonPreview(target.serverName, entry),
4796
- target,
4797
- transport: options.transport
4798
- };
4799
- }
4800
- function formatMcpQuickStart(options) {
4801
- const suffix = options.transport === "http" && options.httpUrl ? ` --transport http --url ${options.httpUrl}` : "";
4802
- return [
4803
- "在 AI 工具中接入 weapp-vite MCP:",
4804
- ` - Codex: wv mcp init codex${suffix}`,
4805
- ` - Claude Code: wv mcp init claude-code${suffix}`,
4806
- ` - Cursor: wv mcp init cursor${suffix}`
4807
- ];
4808
- }
4809
- //#endregion
4810
- //#region src/cli/mcpClient/index.ts
4811
- async function writeMcpClientConfig(plan) {
4812
- await fs$1.ensureDir(path.dirname(plan.target.configPath));
4813
- const existing = await fs$1.readFile(plan.target.configPath, "utf8").catch(() => "");
4814
- if (plan.target.client === "codex") {
4815
- const nextContent = upsertCodexManagedBlock(existing, plan.target.serverName, plan.preview);
4816
- await fs$1.writeFile(plan.target.configPath, nextContent, "utf8");
4817
- return;
4818
- }
4819
- const parsed = parseJsonConfig(existing, plan.target.configPath);
4820
- const nextConfig = {
4821
- ...parsed,
4822
- mcpServers: {
4823
- ...parsed.mcpServers ?? {},
4824
- [plan.target.serverName]: plan.entry
4825
- }
4826
- };
4827
- await fs$1.writeFile(plan.target.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
4828
- }
4829
- //#endregion
4830
- //#region src/cli/commands/mcp.ts
4831
- function resolvePort(port) {
4832
- if (typeof port === "number" && Number.isInteger(port)) return port;
4833
- if (typeof port === "string" && port.trim()) {
4834
- const parsed = Number.parseInt(port, 10);
4835
- if (Number.isInteger(parsed)) return parsed;
4836
- }
4837
- }
4838
- async function resolveHttpUrl(options) {
4839
- if (options.url?.trim()) return options.url.trim();
4840
- const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
4841
- const originalCwd = process.cwd();
4842
- try {
4843
- if (workspaceRoot !== originalCwd) process.chdir(workspaceRoot);
4844
- const loaded = await loadConfig(resolveConfigFile(options));
4845
- const resolved = resolveWeappMcpConfig(loaded?.config?.weapp?.mcp);
4846
- return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
4847
- } catch {
4848
- const resolved = resolveWeappMcpConfig(void 0);
4849
- return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
4850
- } finally {
4851
- if (process.cwd() !== originalCwd) process.chdir(originalCwd);
4852
- }
4853
- }
4854
- async function confirmWrite() {
4855
- if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
4856
- return await runWithSuspendedSharedInput(async () => {
4857
- const rl = createInterface({
4858
- input: process.stdin,
4859
- output: process.stdout
4860
- });
4861
- try {
4862
- const normalized = (await rl.question("是否写入配置文件?(Y/n) ")).trim().toLowerCase();
4863
- return normalized === "" || normalized === "y" || normalized === "yes";
4864
- } finally {
4865
- rl.close();
4866
- }
4867
- });
4868
- }
4869
- function resolveClientTransport(transport) {
4870
- return transport === "http" ? "http" : "command";
4871
- }
4872
- async function handlePrint(clientName, options, write = false) {
4873
- const client = resolveSupportedMcpClient(clientName);
4874
- const workspaceRoot = options.workspaceRoot ?? process.cwd();
4875
- const transport = resolveClientTransport(options.transport);
4876
- const plan = buildMcpClientConfigPlan({
4877
- client,
4878
- transport,
4879
- url: transport === "http" ? await resolveHttpUrl(options) : void 0,
4880
- workspaceRoot
4881
- });
4882
- logger_default.info(`${plan.target.displayName} 配置文件:${plan.target.configPath}`);
4883
- logger_default.info(`服务器名称:${plan.target.serverName}`);
4884
- process.stdout.write(`${plan.preview}\n`);
4885
- if (!write) return;
4886
- if (options.yes || await confirmWrite()) {
4887
- await writeMcpClientConfig(plan);
4888
- logger_default.success(`已写入 ${plan.target.displayName} MCP 配置。`);
4889
- logger_default.info(`请重启 ${plan.target.displayName},然后执行:wv mcp doctor ${client}`);
4890
- return;
4891
- }
4892
- logger_default.info("已取消写入。");
4893
- }
4894
- async function handleDoctor(clientName, options) {
4895
- const result = await inspectMcpClientConfig({
4896
- client: resolveSupportedMcpClient(clientName),
4897
- workspaceRoot: options.workspaceRoot ?? process.cwd()
4898
- });
4899
- logger_default.info(`${result.displayName} 配置文件:${result.configPath}`);
4900
- logger_default.info(`服务器名称:${result.serverName}`);
4901
- if (!result.configExists || result.issues.length > 0) {
4902
- for (const issue of result.issues) logger_default.warn(issue);
4903
- throw new Error("MCP 客户端配置检查未通过。");
4904
- }
4905
- if (result.transport) logger_default.info(`传输模式:${result.transport}`);
4906
- if (result.httpReachable !== void 0) logger_default.info(`HTTP 服务可达:${result.httpReachable ? "是" : "否"}`);
4907
- logger_default.success("MCP 客户端配置检查通过。");
4908
- }
4909
- async function handleServer(options) {
4910
- const resolvedTransport = options.transport === "http" ? "streamable-http" : options.transport === "command" ? void 0 : options.transport;
4911
- await startWeappViteMcpServer({
4912
- endpoint: options.endpoint,
4913
- host: options.host,
4914
- port: resolvePort(options.port),
4915
- restEndpoint: options.rest === false ? false : options.restEndpoint,
4916
- transport: resolvedTransport,
4917
- unref: options.unref,
4918
- workspaceRoot: options.workspaceRoot
4919
- });
4920
- for (const line of formatMcpQuickStart({
4921
- httpUrl: resolvedTransport === "streamable-http" ? `http://${options.host ?? "127.0.0.1"}:${resolvePort(options.port) ?? 3088}${options.endpoint ?? "/mcp"}` : void 0,
4922
- transport: resolvedTransport === "streamable-http" ? "http" : "command"
4923
- })) logger_default.info(line);
4924
- }
4925
- function registerMcpCommand(cli) {
4926
- 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) => {
4927
- const [subcommand, client] = args;
4928
- if (subcommand === "init") {
4929
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp init <codex|claude-code|cursor>");
4930
- await handlePrint(client, options, true);
4931
- return;
4932
- }
4933
- if (subcommand === "print") {
4934
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp print <codex|claude-code|cursor>");
4935
- await handlePrint(client, options, false);
4936
- return;
4937
- }
4938
- if (subcommand === "doctor") {
4939
- if (!client) throw new Error("缺少客户端名称,请使用:wv mcp doctor <codex|claude-code|cursor>");
4940
- await handleDoctor(client, options);
4941
- return;
4942
- }
4943
- if (subcommand) throw new Error(`未知的 mcp 子命令:${subcommand}`);
4944
- await handleServer(options);
4945
- });
4946
- }
4947
- //#endregion
4948
4575
  //#region src/cli/commands/npm.ts
4949
4576
  function registerNpmCommand(cli) {
4950
4577
  cli.command("npm").alias("build:npm").alias("build-npm").action(async () => {
@@ -5066,7 +4693,7 @@ function applyMcpCliOptions(config, options) {
5066
4693
  //#endregion
5067
4694
  //#region src/cli/commands/open.ts
5068
4695
  function registerOpenCommand(cli) {
5069
- 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) => {
5070
4697
  filterDuplicateOptions(options);
5071
4698
  const configFile = resolveConfigFile(options);
5072
4699
  const targets = resolveRuntimeTargets(options);
@@ -5369,9 +4996,6 @@ function resolveRunnableHotkeyDefinition(input) {
5369
4996
  return DEV_HOTKEY_DEFINITIONS.find((action) => action.run && action.key === input);
5370
4997
  }
5371
4998
  //#endregion
5372
- //#region package.json
5373
- var version = "7.3.0";
5374
- //#endregion
5375
4999
  //#region src/cli/devHotkeys/format.ts
5376
5000
  const FULLWIDTH_ASCII_START = 65281;
5377
5001
  const FULLWIDTH_ASCII_END = 65374;
@@ -5531,6 +5155,8 @@ function startDevHotkeys(options) {
5531
5155
  if (options.platform !== "weapp" || !process.stdin.isTTY) return;
5532
5156
  let closed = false;
5533
5157
  let running = false;
5158
+ let closing;
5159
+ let actionPromise;
5534
5160
  let mcpHandle;
5535
5161
  let onData;
5536
5162
  let onKeypress;
@@ -5565,17 +5191,25 @@ function startDevHotkeys(options) {
5565
5191
  }
5566
5192
  });
5567
5193
  const close = () => {
5568
- if (closed) return;
5194
+ if (closing) return closing;
5569
5195
  closed = true;
5570
5196
  if (onSigcont) process.off("SIGCONT", onSigcont);
5571
- inputSession?.close();
5197
+ const inputErrors = [];
5198
+ try {
5199
+ inputSession?.close();
5200
+ } catch (error) {
5201
+ inputErrors.push(error);
5202
+ }
5572
5203
  inputSession = void 0;
5573
- if (mcpHandle?.close) mcpHandle.close().catch((error) => {
5574
- logger_default.warn(`[dev action] MCP 服务关闭失败:${error instanceof Error ? error.message : String(error)}`);
5575
- });
5576
- closeSharedMiniProgram(options.projectPath).catch((error) => {
5577
- logger_default.warn(`[dev action] DevTools 会话关闭失败:${error instanceof Error ? error.message : String(error)}`);
5578
- });
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
+ })();
5579
5213
  };
5580
5214
  const printPanel = (message, force = false) => {
5581
5215
  if (!force && message === lastRenderedPanel) return;
@@ -5611,36 +5245,21 @@ function startDevHotkeys(options) {
5611
5245
  }
5612
5246
  });
5613
5247
  const resolvePendingLabel = (input) => {
5614
- if (input === "m") return mcpHandle?.close ? "正在关闭 MCP 服务" : "正在启动 MCP 服务";
5615
- };
5616
- const runAction = (label, pendingLabel, action) => {
5617
- if (running) {
5618
- const current = currentAction ?? "已有命令";
5619
- const elapsed = actionStartedAt === void 0 ? "" : `(已运行 ${Math.max(0, Date.now() - actionStartedAt)} ms)`;
5620
- logger_default.warn(`[dev action] 当前正在${current.replace(REG_PENDING_PREFIX, "")}${elapsed},已忽略重复请求。`);
5621
- return;
5622
- }
5623
- running = true;
5624
- actionStartedAt = Date.now();
5625
- currentAction = pendingLabel;
5626
- printHint();
5627
- action().then((summary) => {
5628
- if (summary) lastAction = summary;
5629
- }).catch((error) => {
5630
- logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
5631
- }).finally(() => {
5632
- running = false;
5633
- actionStartedAt = void 0;
5634
- currentAction = void 0;
5635
- if (!closed) printHint();
5636
- });
5248
+ if (input === "m") return mcpHandle?.close ? "正在关闭 MCP 服务" : "正在启动 MCP 服务";
5637
5249
  };
5638
- const runBackgroundAction = (label, pendingLabel, action) => {
5639
- if (running) return;
5250
+ const runAction = (label, pendingLabel, action, quiet = false) => {
5251
+ if (running) {
5252
+ if (quiet) return;
5253
+ const current = currentAction ?? "已有命令";
5254
+ const elapsed = actionStartedAt === void 0 ? "" : `(已运行 ${Math.max(0, Date.now() - actionStartedAt)} ms)`;
5255
+ logger_default.warn(`[dev action] 当前正在${current.replace(REG_PENDING_PREFIX, "")}${elapsed},已忽略重复请求。`);
5256
+ return;
5257
+ }
5640
5258
  running = true;
5641
5259
  actionStartedAt = Date.now();
5642
5260
  currentAction = pendingLabel;
5643
- action().then((summary) => {
5261
+ if (!quiet) printHint();
5262
+ actionPromise = action().then((summary) => {
5644
5263
  if (summary) lastAction = summary;
5645
5264
  }).catch((error) => {
5646
5265
  logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
@@ -5648,13 +5267,14 @@ function startDevHotkeys(options) {
5648
5267
  running = false;
5649
5268
  actionStartedAt = void 0;
5650
5269
  currentAction = void 0;
5270
+ if (!closed && !quiet) printHint();
5651
5271
  });
5652
5272
  };
5653
5273
  const handleInput = (input) => {
5654
5274
  if (closed) return;
5655
5275
  const normalizedInput = normalizeInputChar(input);
5656
5276
  if (normalizedInput === "") {
5657
- close();
5277
+ close().catch((error) => logger_default.error(error));
5658
5278
  forwardSigint();
5659
5279
  return;
5660
5280
  }
@@ -5664,7 +5284,7 @@ function startDevHotkeys(options) {
5664
5284
  }
5665
5285
  const normalized = normalizedInput.toLowerCase();
5666
5286
  if (normalized === "q") {
5667
- close();
5287
+ close().catch((error) => logger_default.error(error));
5668
5288
  forwardSigint();
5669
5289
  return;
5670
5290
  }
@@ -5714,9 +5334,9 @@ function startDevHotkeys(options) {
5714
5334
  };
5715
5335
  process.on("SIGCONT", onSigcont);
5716
5336
  if (!options.silentStartupHint) printHint();
5717
- if (resolvedMcp.enabled && resolvedMcp.autoStart) runBackgroundAction("MCP 自动启动", "正在启动 MCP 服务", async () => {
5337
+ if (resolvedMcp.enabled && resolvedMcp.autoStart) runAction("MCP 自动启动", "正在启动 MCP 服务", async () => {
5718
5338
  await toggleMcp({ silent: true });
5719
- });
5339
+ }, true);
5720
5340
  return {
5721
5341
  close,
5722
5342
  restore,
@@ -5724,6 +5344,127 @@ function startDevHotkeys(options) {
5724
5344
  };
5725
5345
  }
5726
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
5727
5468
  //#region src/cli/commands/serve/analyze.ts
5728
5469
  const REG_DIST_PAGE_ENTRY = /pages\/.+\/index\.js$/;
5729
5470
  const REG_DIST_POSIX_SEP = /\\/g;
@@ -5838,7 +5579,7 @@ async function analyzeUiFallback(ctx, options) {
5838
5579
  };
5839
5580
  }
5840
5581
  function createAnalyzeController(options) {
5841
- const { configFile, ctx, options: cliOptions, targets } = options;
5582
+ const { configFile, ctx, options: cliOptions, targets, shutdown } = options;
5842
5583
  const { configService } = ctx;
5843
5584
  let analyzeRunId = 0;
5844
5585
  let analyzeHandle;
@@ -5889,8 +5630,8 @@ function createAnalyzeController(options) {
5889
5630
  }
5890
5631
  return runFallback("完整分析结果为空,已回退到 dist 文件扫描。");
5891
5632
  };
5892
- const triggerAnalyzeUpdate = async (reason = "watch") => {
5893
- if (!analyzeHandle) return;
5633
+ const runAnalyzeUpdate = async (reason) => {
5634
+ if (!analyzeHandle || shutdown?.stopping) return;
5894
5635
  emitDashboardEvents(analyzeHandle, [{
5895
5636
  kind: reason === "watch" ? "hmr" : "build",
5896
5637
  level: "info",
@@ -5899,6 +5640,7 @@ function createAnalyzeController(options) {
5899
5640
  tags: reason === "watch" ? ["watch", "analyze"] : ["initial", "analyze"]
5900
5641
  }]);
5901
5642
  const next = await runAnalyze();
5643
+ if (shutdown?.stopping) return;
5902
5644
  if (next.mode === "fallback") emitDashboardEvents(analyzeHandle, [{
5903
5645
  kind: "diagnostic",
5904
5646
  level: "warning",
@@ -5917,6 +5659,10 @@ function createAnalyzeController(options) {
5917
5659
  tags: next.mode === "fallback" ? ["analyze", "fallback"] : ["analyze", reason === "watch" ? "refresh" : "initial"]
5918
5660
  }]);
5919
5661
  };
5662
+ const triggerAnalyzeUpdate = (reason = "watch") => {
5663
+ if (shutdown?.stopping) return Promise.resolve();
5664
+ return shutdown ? shutdown.run("startup", () => runAnalyzeUpdate(reason)) : runAnalyzeUpdate(reason);
5665
+ };
5920
5666
  const bindWatcher = (buildResult) => {
5921
5667
  let initialUpdatePromise;
5922
5668
  let watchUpdatePending = false;
@@ -5946,6 +5692,7 @@ function createAnalyzeController(options) {
5946
5692
  return watchUpdatePromise;
5947
5693
  };
5948
5694
  if (analyzeHandle && buildResult && typeof buildResult.on === "function") buildResult.on("event", (event) => {
5695
+ if (shutdown?.stopping) return;
5949
5696
  if (event.code === "ERROR") {
5950
5697
  emitDashboardEvents(analyzeHandle, [{
5951
5698
  kind: "diagnostic",
@@ -5969,8 +5716,10 @@ function createAnalyzeController(options) {
5969
5716
  getHandle: () => analyzeHandle,
5970
5717
  async startDashboard(startDashboard) {
5971
5718
  const initialAnalyze = await runAnalyze();
5719
+ if (shutdown?.stopping) return;
5972
5720
  analyzeHandle = await startDashboard(initialAnalyze.result, {
5973
5721
  watch: true,
5722
+ uiHost: cliOptions.uiHost,
5974
5723
  artifacts: initialAnalyze.artifacts,
5975
5724
  cwd: configService.cwd,
5976
5725
  packageManagerAgent: configService.packageManager.agent,
@@ -6022,273 +5771,277 @@ function createAnalyzeController(options) {
6022
5771
  };
6023
5772
  }
6024
5773
  //#endregion
6025
- //#region src/cli/commands/serve/shared.ts
6026
- function startBackgroundForwardConsole(startForwardConsole, options) {
6027
- if (!startForwardConsole) return;
6028
- try {
6029
- startForwardConsole(options).catch((error) => {
6030
- const message = error instanceof Error ? error.message : String(error);
6031
- 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
6032
5907
  });
6033
- } catch (error) {
6034
- const message = error instanceof Error ? error.message : String(error);
6035
- 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
+ }
6036
5928
  }
6037
- }
6038
- function resolveServeIdeOpenStrategy(options = {}, fallback = "cli") {
6039
- return options.openStrategy ?? (options.useAutomatorOpen ? "automator" : fallback);
6040
- }
6041
- function resolveWebHost(host) {
6042
- if (host === void 0) return;
6043
- if (typeof host === "boolean") return host;
6044
- if (typeof host === "string") return host;
6045
- return String(host);
6046
- }
6047
- /**
6048
- * @description 为 serve 模式构造统一的小程序开发动作,供热键与命令复用。
6049
- */
6050
- function createServeMiniProgramDevActions(options) {
6051
- const projectPath = options.projectPath ?? options.fallbackProjectPath;
6052
5929
  return {
6053
- projectPath,
6054
- openIde: async (openOptions = {}) => {
6055
- if (!openOptions.forceOpen && await options.tryReuseForwardConsole?.()) return "已通过控制台转发复用当前开发者工具会话";
6056
- await options.openIde(projectPath, openOptions);
6057
- startBackgroundForwardConsole(options.startForwardConsole, openOptions);
6058
- return openOptions.forceReopen ? "已重新打开微信开发者工具项目" : "已打开或复用微信开发者工具项目";
6059
- },
6060
- rebuild: async () => {
6061
- await options.build();
6062
- return "已手动重新构建当前小程序产物";
6063
- }
5930
+ watch: targets.has("dev"),
5931
+ waitForExit: analyzeHandle ? () => analyzeController.waitForExit() : void 0
6064
5932
  };
6065
5933
  }
6066
- function waitForServeShutdownSignal() {
6067
- return new Promise((resolve) => {
6068
- const onSignal = () => {
6069
- process.off("SIGINT", onSignal);
6070
- process.off("SIGTERM", onSignal);
6071
- resolve();
6072
- };
6073
- process.on("SIGINT", onSignal);
6074
- process.on("SIGTERM", onSignal);
6075
- });
6076
- }
6077
5934
  //#endregion
6078
5935
  //#region src/cli/commands/serve/index.ts
6079
- function writePostOpenSeparator() {
6080
- process.stdout?.write?.("\n");
6081
- }
6082
5936
  function registerServeCommand(cli) {
6083
- 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) => {
6084
- filterDuplicateOptions(options);
6085
- setCommandNodeEnv("development");
6086
- const cwd = root ?? process.cwd();
6087
- const configFile = resolveConfigFile(options);
6088
- const targets = resolveRuntimeTargets(options);
6089
- const webBackend = getBackendForCapability(targets, "web", "dev");
6090
- const host = webBackend ? resolveWebHost(options.host) : void 0;
6091
- const inlineConfig = createInlineConfig(targets, {
6092
- scope: options.scope,
6093
- host
6094
- });
6095
- const ctx = await createCompilerContext({
6096
- cwd,
6097
- mode: options.mode ?? "development",
6098
- isDev: true,
6099
- configFile,
6100
- inlineConfig,
6101
- cliPlatform: targets.rawPlatform,
6102
- preloadAppEntry: false,
6103
- syncAutoImportSupportFiles: false,
6104
- projectConfigPath: options.projectConfig
6105
- });
6106
- const { configService } = ctx;
6107
- const miniBackend = getBackendForCapability(targets, "miniprogram", "dev");
6108
- const aiEnvironment = await detectAiDevelopmentEnvironment();
6109
- const mcpConfig = applyMcpCliOptions(configService.weappViteConfig?.mcp, options);
6110
- logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
6111
- const enableAnalyze = Boolean(isUiEnabled(options) && miniBackend);
6112
- let analyzeHandle;
6113
- const miniProgramDevActions = createServeMiniProgramDevActions({
6114
- build: async () => {
6115
- await miniBackend?.driver.dev(ctx, options);
6116
- },
6117
- fallbackProjectPath: configService.cwd,
6118
- openIde: async (projectPath, openOptions) => {
6119
- const forceReopen = openOptions?.forceReopen === true;
6120
- const openStrategy = resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy);
6121
- const useAutomatorOpen = openStrategy === "automator";
6122
- await openIde(configService.platform, projectPath, {
6123
- loginRetry: options.loginRetry,
6124
- loginRetryTimeout: options.loginRetryTimeout,
6125
- nonInteractive: options.nonInteractive,
6126
- openRecovery: false,
6127
- prepareAutomatorSession: useAutomatorOpen,
6128
- reuseOpenedProject: !forceReopen,
6129
- skipAutomatorCompile: !forceReopen,
6130
- skipPostOpenHealthCheck: true,
6131
- trustProject: options.trustProject,
6132
- openStrategy,
6133
- useAutomatorOpen
6134
- });
6135
- writePostOpenSeparator();
6136
- },
6137
- projectPath: resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd),
6138
- startForwardConsole: async (openOptions) => {
6139
- if (resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy) === "automator") return await maybeStartForwardConsole({
6140
- openedOnly: true,
6141
- preferOpenedSession: true,
6142
- platform: configService.platform,
6143
- mpDistRoot: configService.mpDistRoot,
6144
- cwd: configService.cwd,
6145
- weappViteConfig: configService.weappViteConfig
6146
- });
6147
- return await maybeStartForwardConsole({
6148
- platform: configService.platform,
6149
- mpDistRoot: configService.mpDistRoot,
6150
- cwd: configService.cwd,
6151
- preferOpenedSession: false,
6152
- recoverAutomatorSession: async () => {
6153
- const projectPath = resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd);
6154
- await openIde(configService.platform, projectPath, {
6155
- loginRetry: options.loginRetry,
6156
- loginRetryTimeout: options.loginRetryTimeout,
6157
- nonInteractive: options.nonInteractive,
6158
- openRecovery: false,
6159
- openStrategy: "automator",
6160
- prepareAutomatorSession: true,
6161
- reuseOpenedProject: false,
6162
- skipAutomatorCompile: false,
6163
- skipPostOpenHealthCheck: true,
6164
- trustProject: options.trustProject,
6165
- useAutomatorOpen: true
6166
- });
6167
- },
6168
- weappViteConfig: configService.weappViteConfig
6169
- });
6170
- },
6171
- tryReuseForwardConsole: async () => {
6172
- return await maybeStartForwardConsole({
6173
- openedOnly: true,
6174
- platform: configService.platform,
6175
- mpDistRoot: configService.mpDistRoot,
6176
- cwd: configService.cwd,
6177
- weappViteConfig: configService.weappViteConfig
6178
- });
6179
- }
6180
- });
6181
- const devHotkeysSession = miniBackend ? startDevHotkeys({
6182
- cwd: configService.cwd,
6183
- agentName: aiEnvironment.agentName,
6184
- isAgent: aiEnvironment.isAgent,
6185
- mcpConfig,
6186
- openIde: async () => await miniProgramDevActions.openIde({
6187
- forceOpen: true,
6188
- forceReopen: true
6189
- }),
6190
- platform: configService.platform,
6191
- projectPath: miniProgramDevActions.projectPath ?? configService.cwd,
6192
- rebuild: miniProgramDevActions.rebuild,
6193
- silentStartupHint: true,
6194
- weappViteConfig: configService.weappViteConfig
6195
- }) : 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;
6196
5940
  try {
6197
- const analyzeController = createAnalyzeController({
6198
- configFile,
6199
- ctx,
6200
- options,
6201
- targets
6202
- });
6203
- let webServer;
6204
- for (const backend of targets.select("dev")) {
6205
- if (backend.descriptor.id === "miniprogram") {
6206
- const miniBuildStartedAt = Date.now();
6207
- const buildResult = await backend.driver.dev(ctx, options);
6208
- const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
6209
- logger_default.success(`小程序初次构建完成,耗时:${formatDuration(miniBuildDurationMs)}`);
6210
- ctx.autoImportService?.syncSupportFileResolverComponents().catch((error) => {
6211
- const message = error instanceof Error ? error.message : String(error);
6212
- logger_default.warn(`[prepare] 后台同步 .weapp-vite 支持文件失败:${message}`);
6213
- });
6214
- if (enableAnalyze) {
6215
- await analyzeController.startDashboard(startAnalyzeDashboard);
6216
- analyzeHandle = analyzeController.getHandle();
6217
- analyzeController.emitRuntimeEvents([{
6218
- kind: "build",
6219
- level: "success",
6220
- title: "mini initial build completed",
6221
- detail: "小程序开发态初次构建已完成,dashboard 可继续监听后续刷新。",
6222
- durationMs: miniBuildDurationMs,
6223
- tags: [
6224
- "dev",
6225
- "mini",
6226
- "initial"
6227
- ]
6228
- }]);
6229
- await analyzeController.bindWatcher(buildResult).runInitialUpdate();
6230
- }
6231
- continue;
6232
- }
6233
- if (backend.descriptor.id !== "web") continue;
6234
- const webServerStartedAt = Date.now();
6235
- try {
6236
- webServer = await backend.driver.dev(ctx, options);
6237
- logger_default.success(`Web 开发服务启动完成,耗时:${formatDuration(Date.now() - webServerStartedAt)}`);
6238
- analyzeController.emitRuntimeEvents([{
6239
- kind: "system",
6240
- level: "success",
6241
- title: "web dev server started",
6242
- detail: "Web 开发服务器已启动,可与小程序调试 UI 并行工作。",
6243
- durationMs: Date.now() - webServerStartedAt,
6244
- tags: ["dev", "web"]
6245
- }]);
6246
- } catch (error) {
6247
- analyzeController.emitRuntimeEvents([{
6248
- kind: "diagnostic",
6249
- level: "error",
6250
- title: "web dev server failed",
6251
- detail: error instanceof Error ? error.message : String(error),
6252
- durationMs: Date.now() - webServerStartedAt,
6253
- tags: ["dev", "web"]
6254
- }]);
6255
- logger_default.error(error);
6256
- throw error;
6257
- }
6258
- }
6259
- if (miniBackend) {
6260
- logBuildAppFinish(configService, webServer, {
6261
- skipWeb: !webBackend,
6262
- uiUrls: analyzeHandle?.urls
6263
- });
6264
- devHotkeysSession?.restore();
6265
- } else if (webBackend) logBuildAppFinish(configService, webServer, { skipMini: true });
6266
- if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
6267
- analyzeController.emitRuntimeEvents([{
6268
- kind: "command",
6269
- level: "info",
6270
- title: "opening ide",
6271
- detail: "开发服务已就绪,准备打开 IDE 项目。",
6272
- tags: ["ide", "open"]
6273
- }]);
6274
- devHotkeysSession?.suspend();
6275
- try {
6276
- await miniProgramDevActions.openIde({
6277
- forceOpen: true,
6278
- forceReopen: false,
6279
- openStrategy: options.ideOpenStrategy ?? "cli"
6280
- });
6281
- } finally {
6282
- devHotkeysSession?.restore();
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;
5946
+ }
5947
+ await shutdown.close(failure);
5948
+ });
5949
+ }
5950
+ //#endregion
5951
+ //#region src/cli/commands/upload.ts
5952
+ async function buildUploadTarget(cwd, platform, options, action, item) {
5953
+ const targets = resolveRuntimeTargets({ platform });
5954
+ const ctx = await createCompilerContext({
5955
+ cwd,
5956
+ mode: options.mode ?? "production",
5957
+ configFile: resolveConfigFile(options),
5958
+ inlineConfig: createInlineConfig(targets),
5959
+ cliPlatform: platform,
5960
+ projectConfigPath: options.projectConfig,
5961
+ emitDefaultAutoImportOutputs: false,
5962
+ preloadAppEntry: false
5963
+ });
5964
+ try {
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
+ }
5971
+ logger_default.info(`[${action}:${target.platform}] 构建${target.context.version ? ` ${target.context.version}` : ""}`);
5972
+ await ctx.buildService.build({});
5973
+ return target;
5974
+ } finally {
5975
+ ctx.watcherService.closeAll();
5976
+ terminateStaleSassEmbeddedProcess();
5977
+ }
5978
+ }
5979
+ async function runUploadCommand(root, options, action = "upload") {
5980
+ filterDuplicateOptions(options);
5981
+ const platforms = resolveUploadPlatforms(options.platform ?? options.p);
5982
+ setCommandNodeEnv("production");
5983
+ const cwd = path$1.resolve(root ?? process.cwd());
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";
6283
6012
  }
6284
- }
6285
- if (analyzeHandle) await analyzeController.waitForExit();
6286
- else if (targets.has("dev")) await waitForServeShutdownSignal();
6287
- } finally {
6288
- devHotkeysSession?.close();
6289
- await closeActiveForwardConsole();
6290
- for (const backend of [...targets.select("dev")].reverse()) await backend.driver.close(ctx);
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";
6291
6026
  }
6027
+ throw new UploadCommandError(report);
6028
+ }
6029
+ }
6030
+ function registerUploadCommand(cli) {
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, {
6033
+ ...options,
6034
+ ...readUploadMetadata(cli)
6035
+ });
6036
+ });
6037
+ }
6038
+ function registerPreviewCommand(cli) {
6039
+ cli.command("preview [root]", "build mini programs and generate preview QR codes or links").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("--desc <text>", "[string] preview description").option("--dry-run", "[boolean] build without validating credentials or generating previews").action(async (root, options) => {
6040
+ await runUploadCommand(root, {
6041
+ ...options,
6042
+ ...readUploadMetadata(cli)
6043
+ }, "preview");
6044
+ scheduleCompletedProductionBuildExit({}, void 0);
6292
6045
  });
6293
6046
  }
6294
6047
  //#endregion
@@ -6333,6 +6086,8 @@ const WEAPP_VITE_NATIVE_COMMANDS = /* @__PURE__ */ new Set([
6333
6086
  "dev",
6334
6087
  "serve",
6335
6088
  "build",
6089
+ "upload",
6090
+ "preview",
6336
6091
  "close",
6337
6092
  "analyze",
6338
6093
  "alipay",
@@ -6343,10 +6098,72 @@ const WEAPP_VITE_NATIVE_COMMANDS = /* @__PURE__ */ new Set([
6343
6098
  "build-npm",
6344
6099
  "generate",
6345
6100
  "g",
6346
- "mcp"
6101
+ "mcp",
6102
+ "accept"
6347
6103
  ]);
6104
+ const LEGACY_UPLOAD_OPTIONS = {
6105
+ "--version": true,
6106
+ "-v": true,
6107
+ "--project": true,
6108
+ "--appid": true,
6109
+ "--ext-appid": true,
6110
+ "--info-output": true,
6111
+ "-i": true
6112
+ };
6113
+ const SDK_UPLOAD_OPTIONS = {
6114
+ "--platform": true,
6115
+ "--uv": true,
6116
+ "--bump": true,
6117
+ "--git-desc": true,
6118
+ "--dry-run": true,
6119
+ "--json": true,
6120
+ "--timeout": true
6121
+ };
6122
+ const UPLOAD_VALUE_OPTIONS = {
6123
+ ...LEGACY_UPLOAD_OPTIONS,
6124
+ "-p": true,
6125
+ "--desc": true,
6126
+ "-d": true,
6127
+ "--platform": true,
6128
+ "--uv": true,
6129
+ "--bump": true,
6130
+ "--timeout": true,
6131
+ "--project-config": true,
6132
+ "--port": true,
6133
+ "--lang": true,
6134
+ "-c": true,
6135
+ "--config": true,
6136
+ "--base": true,
6137
+ "-l": true,
6138
+ "--logLevel": true,
6139
+ "-f": true,
6140
+ "--filter": true,
6141
+ "-m": true,
6142
+ "--mode": true
6143
+ };
6144
+ /**
6145
+ * @description 仅按显式参数区分上传方言,不根据 -p 的值或文件系统猜测后端。
6146
+ */
6147
+ function isLegacyUploadInvocation(argv) {
6148
+ let legacyOption;
6149
+ let sdkOption;
6150
+ for (let index = 1; index < argv.length; index += 1) {
6151
+ const token = argv[index];
6152
+ if (token === "--") break;
6153
+ const equalsIndex = token.indexOf("=");
6154
+ const option = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
6155
+ if (LEGACY_UPLOAD_OPTIONS[option] === true) legacyOption = option;
6156
+ if (SDK_UPLOAD_OPTIONS[option.startsWith("--no-") ? `--${option.slice(5)}` : option] === true) sdkOption = option;
6157
+ if (legacyOption && sdkOption) throw new Error(`不能混用微信 IDE 上传参数 ${legacyOption} 与 SDK 上传参数 ${sdkOption};请使用 wv ide upload 保留 IDE 行为,或使用 wv build --upload 迁移到 SDK。`);
6158
+ if (equalsIndex === -1 && UPLOAD_VALUE_OPTIONS[option] === true) index += 1;
6159
+ }
6160
+ return legacyOption !== void 0;
6161
+ }
6162
+ function warnLegacyUpload() {
6163
+ logger_default.warn("顶层微信 IDE 上传语法已弃用,将在未来版本移除。请迁移到 wv build --upload -p weapp --uv <version> --desc <description>(SDK 流程需要源码目录与上传凭据,并非 IDE 命令的等价替换);或使用 wv ide upload ... 保留现有 IDE 行为。");
6164
+ }
6348
6165
  /**
6349
- * @description 让 weapp-vite 在未命中自身命令时,回退到 weapp-ide-cli。
6166
+ * @description 保留旧版上传方言;其余未命中自身命令的调用回退到 weapp-ide-cli。
6350
6167
  */
6351
6168
  async function tryRunIdeCommand(argv) {
6352
6169
  const command = argv[0];
@@ -6359,11 +6176,14 @@ async function tryRunIdeCommand(argv) {
6359
6176
  if (command.startsWith("-")) return false;
6360
6177
  if (command === "help") {
6361
6178
  const target = argv[1];
6362
- if (!target || WEAPP_VITE_NATIVE_COMMANDS.has(target) || !isWeappIdeTopLevelCommand(target)) return false;
6179
+ if (!target || target !== "upload" && WEAPP_VITE_NATIVE_COMMANDS.has(target) || !isWeappIdeTopLevelCommand(target)) return false;
6180
+ if (target === "upload") warnLegacyUpload();
6363
6181
  await executeWechatIdeCliCommand(argv);
6364
6182
  return true;
6365
6183
  }
6366
- if (WEAPP_VITE_NATIVE_COMMANDS.has(command) || !isWeappIdeTopLevelCommand(command)) return false;
6184
+ const legacyUpload = command === "upload" && isLegacyUploadInvocation(argv);
6185
+ if (!legacyUpload && WEAPP_VITE_NATIVE_COMMANDS.has(command) || !isWeappIdeTopLevelCommand(command)) return false;
6186
+ if (legacyUpload) warnLegacyUpload();
6367
6187
  if (!await dispatchWechatCliCommand(argv)) await executeWechatIdeCliCommand(argv);
6368
6188
  return true;
6369
6189
  }
@@ -6450,17 +6270,14 @@ function handlePrepareLifecycleError(args, error) {
6450
6270
  //#endregion
6451
6271
  //#region src/cli.ts
6452
6272
  const cli = cac("weapp-vite");
6453
- try {
6454
- checkRuntime({
6455
- bun: "0.0.0",
6456
- deno: "0.0.0",
6457
- node: "^20.19.0 || >=22.12.0"
6458
- });
6459
- } catch {}
6460
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);
6461
6275
  registerIdeCommand(cli);
6276
+ registerDoctorCommand(cli);
6462
6277
  registerAlipayCommand(cli);
6463
6278
  registerBuildCommand(cli);
6279
+ registerUploadCommand(cli);
6280
+ registerPreviewCommand(cli);
6464
6281
  registerCloseCommand(cli);
6465
6282
  registerAnalyzeCommand(cli);
6466
6283
  registerInitCommand(cli);
@@ -6479,7 +6296,9 @@ const skipManagedTsconfigBootstrapCommands = /* @__PURE__ */ new Set([
6479
6296
  "init",
6480
6297
  "mcp",
6481
6298
  "npm",
6482
- "alipay"
6299
+ "alipay",
6300
+ "doctor",
6301
+ "accept"
6483
6302
  ]);
6484
6303
  function resolveManagedTsconfigBootstrapRoot(args) {
6485
6304
  const [firstArg, secondArg] = args;
@@ -6499,7 +6318,9 @@ function resolveManagedTsconfigBootstrapRoot(args) {
6499
6318
  "dev",
6500
6319
  "open",
6501
6320
  "prepare",
6502
- "serve"
6321
+ "preview",
6322
+ "serve",
6323
+ "upload"
6503
6324
  ].includes(firstArg)) {
6504
6325
  if (secondArg && !secondArg.startsWith("-")) return path.resolve(secondArg);
6505
6326
  return process.cwd();
@@ -6509,11 +6330,19 @@ function resolveManagedTsconfigBootstrapRoot(args) {
6509
6330
  try {
6510
6331
  const args = process.argv.slice(2);
6511
6332
  if (!await tryRunIdeCommand(args)) {
6512
- const managedTsconfigBootstrapRoot = resolveManagedTsconfigBootstrapRoot(args);
6513
- if (managedTsconfigBootstrapRoot) await syncManagedTsconfigBootstrapFiles(managedTsconfigBootstrapRoot);
6514
6333
  cli.parse(process.argv, { run: false });
6515
- await maybeAutoStartMcpServer(args, cli.options);
6516
- 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();
6517
6346
  }
6518
6347
  } catch (error) {
6519
6348
  if (!handlePrepareLifecycleError(process.argv.slice(2), error)) {