weapp-vite
Version:
weapp-vite 一个现代化的小程序打包工具
6,188 lines • 253 kB
JavaScript
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 createGlassEaselAnalyzeResult, b as resolveWeappConfigFile, f as getBackendForCapability, g as createSharedBuildConfig, h as syncManagedTsconfigBootstrapFiles, j as shouldPassPlatformArgToIdeOpen, k as parseCommentJson, m as syncProjectSupportFiles, o as formatBytes, p as resolveBackendExecution, t as createCompilerContext, v as resolveHmrProfileJsonPath, w as findPreloadRuleKey, x as checkRuntime } from "./createContext-BYZnlk4j.mjs";
import { f as VERSION, i as findJsEntry, o as findTemplateEntry, s as findVueEntry } from "./file-CLo_LdAz.mjs";
import { r as logger_default, t as colors } from "./logger-mt4mSTqV.mjs";
import { c as startWeappViteMcpServer, l as detectAiDevelopmentEnvironment, s as resolveWeappMcpConfig } from "./mcp-BG6TliEg.mjs";
import { createRequire } from "node:module";
import fs from "node:fs";
import path, { posix } from "pathe";
import path$1 from "node:path";
import { defu } from "@weapp-core/shared";
import { fs as fs$1 } from "@weapp-core/shared/fs";
import process from "node:process";
import { parse } from "vue/compiler-sfc";
import fs$2, { mkdir, writeFile } from "node:fs/promises";
import { build, createServer } from "vite";
import os from "node:os";
import { execFile, spawn } from "node:child_process";
import { Buffer } from "node:buffer";
import { cac } from "cac";
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";
import { promisify } from "node:util";
import { brotliCompressSync, gzipSync } from "node:zlib";
import { resolveCommand } from "package-manager-detector/commands";
import { generateJs, generateJson, generateWxml, generateWxss } from "@weapp-core/schematics";
import { determineAgent } from "@vercel/detect-agent";
import { initConfig } from "@weapp-core/init";
import { createInterface } from "node:readline/promises";
import { clearTimeout as clearTimeout$1, setTimeout as setTimeout$1 } from "node:timers";
import net from "node:net";
//#region src/cli/runtime.ts
function logRuntimeTarget(targets, options = {}) {
if (options.silent) return;
if (targets.label === "config") {
const resolvedPlatform = targets.platform ?? options.resolvedConfigPlatform;
if (resolvedPlatform) {
logger_default.info(`目标平台:${colors.green(resolvedPlatform)}`);
return;
}
logger_default.info(`目标平台:使用配置文件中的 ${colors.bold(colors.green("weapp.platform"))}`);
return;
}
logger_default.info(`目标平台:${colors.green(targets.label)}`);
}
function resolveRuntimeTargets(options) {
const rawPlatform = typeof options.platform === "string" ? options.platform : typeof options.p === "string" ? options.p : void 0;
const execution = resolveBackendExecution(rawPlatform, { warn: (message) => logger_default.warn(message) });
const miniBackend = execution.get("miniprogram");
return {
...execution,
platform: miniBackend?.platform,
rawPlatform
};
}
function createInlineConfig(execution, options = {}) {
const configs = execution.entries.map((entry) => entry.driver.createInlineConfig({
execution,
platform: entry.platform,
scope: options.scope,
host: options.host
})).filter((config) => Boolean(config));
if (configs.length === 0) return options.inlineConfig;
const merged = configs.slice(1).reduce((merged, config) => defu(merged, config), configs[0]);
return options.inlineConfig ? defu(options.inlineConfig, merged) : merged;
}
//#endregion
//#region src/cli/openIde/execute.ts
function readArgOption(argv, ...names) {
for (let index = 0; index < argv.length; index += 1) {
const current = argv[index];
if (!names.includes(current)) continue;
const next = argv[index + 1];
if (typeof next === "string" && !next.startsWith("-")) return next;
}
}
async function tryExecuteWechatIdeCliCommandByAutomator(argv, projectPath, options = {}) {
if (!projectPath) return false;
const command = argv[0];
if (!command) return false;
if (command === "compile") {
await compileWechatIdeByAutomator({
preserveProjectRoot: options.preserveProjectRoot,
projectPath
});
return true;
}
if (command === "cache") {
const cleanType = readArgOption(argv, "--clean", "-c");
if (cleanType !== "compile" && cleanType !== "all") return false;
await clearWechatIdeCacheByAutomator({
clean: cleanType,
preserveProjectRoot: options.preserveProjectRoot,
projectPath
});
return true;
}
return false;
}
async function tryExecuteWechatIdeCliCommandByHttp(argv, projectPath, engineBuildFallbackToCli = false) {
const command = argv[0];
if (!command) return false;
if (command === "reset-fileutils") {
if (!projectPath) return false;
await resetWechatIdeFileUtilsByHttp(projectPath);
return true;
}
if (command === "engine" && argv[1] === "build") {
const engineProjectPath = argv[2] || projectPath;
if (!engineProjectPath) return false;
await runWechatIdeEngineBuild(engineProjectPath, {
fallbackToCli: engineBuildFallbackToCli,
logPath: readArgOption(argv, "--logPath", "-l")
});
return true;
}
return false;
}
async function tryExecuteWechatIdeCliCommandByHelper(argv) {
const command = argv[0];
if (!command) return false;
if (command === "close") {
await closeWechatIdeProject();
return true;
}
if (command === "quit") {
await quitWechatIde();
return true;
}
if (command === "cache") {
const cleanType = readArgOption(argv, "--clean", "-c");
if (!cleanType) return false;
await clearWechatIdeCache({ clean: cleanType });
return true;
}
return false;
}
/**
* @description 统一执行 weapp-ide-cli 命令,并在登录失效时复用同一套重试交互。
*/
async function executeWechatIdeCliCommand(argv, options = {}) {
const { automatorMode = "prefer", cancelLevel = "warn", engineBuildFallbackToCli = false, httpMode = "prefer", promptOpenIdeLogin = true, onNonLoginError, onRetry, projectPath, preserveProjectRoot } = options;
await runWithSuspendedSharedInput(async () => {
if (httpMode !== "skip") try {
if (await tryExecuteWechatIdeCliCommandByHttp(argv, projectPath, engineBuildFallbackToCli)) return;
} catch (error) {
if (httpMode === "require" || isWechatIdeEngineBuildEndpointMissingError(error)) throw error;
}
if (automatorMode !== "skip") try {
if (await tryExecuteWechatIdeCliCommandByAutomator(argv, projectPath, { preserveProjectRoot })) return;
} catch (error) {
if (automatorMode === "require") throw error;
}
try {
if (await tryExecuteWechatIdeCliCommandByHelper(argv)) return;
} catch (error) {
if (isWechatIdeLoginRequiredExitError(error)) throw error;
if (onNonLoginError) {
onNonLoginError(error);
return;
}
throw error;
}
await runRetryableCommand({
createCancelError: (error) => createWechatIdeLoginRequiredExitError(error, "cancelled"),
execute: async () => {
try {
await parse$1(argv);
return null;
} catch (error) {
if (isWechatIdeLoginRequiredExitError(error)) throw error;
if (!isWechatIdeLoginRequiredError(error)) {
if (onNonLoginError) {
onNonLoginError(error);
return null;
}
throw error;
}
return error;
}
},
isRetryableResult: (result) => result !== null,
onCancel: () => {},
onRetry: () => {
onRetry?.();
},
promptRetry: async (error) => await promptWechatIdeLoginRetry({
cancelLevel,
error,
logger: logger_default,
promptOpenIdeLogin
}),
shouldRetry: (action) => action === "retry"
});
});
}
//#endregion
//#region src/cli/openIde/close.ts
const execFileAsync = promisify(execFile);
async function closeIdeByAppleScript() {
if (process.platform !== "darwin") return false;
const appName = process.env.WEAPP_DEVTOOLS_APP_NAME || "wechatwebdevtools";
try {
await execFileAsync("osascript", ["-e", `tell application "${appName}" to quit`]);
return true;
} catch {
return false;
}
}
async function closeIdeByProcessKill(cliPath) {
if (!cliPath) return false;
const appContentsRoot = cliPath.includes(".app/") ? cliPath.slice(0, cliPath.indexOf(".app/") + 4) : path.dirname(path.dirname(cliPath));
try {
await execFileAsync("pkill", ["-f", appContentsRoot]);
return true;
} catch {
return false;
}
}
/**
* @description 关闭微信开发者工具,并在 CLI 不可用时回退到系统级关闭。
*/
async function closeIde$1() {
const config = await getConfig();
const cliPath = config.cliPath?.trim() ? config.cliPath : null;
try {
await closeWechatIdeProject();
return true;
} catch (error) {
if (isWechatIdeLoginRequiredError(error)) try {
await executeWechatIdeCliCommand(["close"], {
cancelLevel: "warn",
onNonLoginError: (retryError) => logger_default.error(retryError),
onRetry: () => logger_default.info("正在重试连接微信开发者工具...")
});
return true;
} catch (retryError) {
logger_default.error(retryError);
}
else {
logger_default.warn("微信开发者工具 CLI close 执行失败,尝试回退为系统级关闭。");
logger_default.error(error);
}
if (await closeIdeByAppleScript()) {
logger_default.info("已回退为系统级关闭微信开发者工具。");
return true;
}
if (await closeIdeByProcessKill(cliPath)) {
logger_default.info("已回退为进程级关闭微信开发者工具。");
return true;
}
return false;
}
}
//#endregion
//#region src/cli/openIde/diagnostics.ts
/**
* @description 将 automator 启动错误归类为用户可执行的恢复提示。
*/
function formatWechatIdeAutomatorFailure(error) {
const message = error instanceof Error ? error.message : String(error);
if (/需要重新登录|need\s+re-?login|code\s*[:=]\s*10/i.test(message)) return "登录状态失效,请先登录微信开发者工具。";
if (/service port|http port|ECONNREFUSED|listen EPERM|EACCES/i.test(message)) return "服务端口不可用,请在设置 -> 安全设置中开启服务端口。";
if (/protocol method|protocol timeout|automator.*timeout|Wait timed out|websocket|ws:\/\//i.test(message)) return "自动化 websocket 或协议尚未就绪,已保留普通 CLI 打开的项目。";
if (/cliPath|not found|spawn/i.test(message)) return "微信开发者工具 CLI 路径不可用,请执行 `wv ide doctor` 检查配置。";
return "当前 DevTools 自动化能力不可用,已保留普通 CLI 打开的项目。";
}
/**
* @description 输出微信开发者工具打开后的自助恢复步骤。
*/
function logWechatIdeRecoveryHint(options) {
const lines = [
`微信开发者工具打开后状态可能不稳定:${options.reason}`,
"可按下面顺序恢复:",
"1. 在微信开发者工具中确认:设置 -> 安全设置 -> 服务端口已开启。",
"2. 仅当项目索引刷新失败时,默认会自动关闭并重开一次当前目标项目;如需跳过,传入 `--no-open-recovery` 或设置 `WEAPP_VITE_DISABLE_IDE_OPEN_RECOVERY=1`。",
"3. 如果仍然回到项目选择页,手动导入 project.config.json 所在目录,并关闭多余的微信开发者工具窗口后重试。",
"4. 需要查看底层错误时,设置 `WEAPP_VITE_DEBUG_AUTOMATOR_OPEN=1` 后重试。"
];
if (options.projectPath) lines.push(`当前目标项目目录:${options.projectPath}`);
logger_default.warn(lines.join("\n"));
}
/**
* @description 输出服务端口关闭时的定向恢复提示。
*/
function logWechatIdeServicePortDisabledHint(projectPath) {
const lines = [
"检测到微信开发者工具服务端口当前处于关闭状态,已保留用户设置并回退到普通 open 流程。",
"自动刷新、截图、MCP 和 IDE 联动能力需要服务端口。",
"请在微信开发者工具中打开:设置 -> 安全设置 -> 服务端口,然后重新执行当前 dev/open 命令。"
];
if (projectPath) lines.push(`当前目标项目目录:${projectPath}`);
logger_default.warn(lines.join("\n"));
}
//#endregion
//#region src/cli/openIde/reuse.ts
const OPENED_PROJECT_HEALTH_CHECK_TIMEOUT = 3e3;
const OPEN_AUTOMATOR_TIMEOUT = 12e4;
function formatReuseOpenedWechatIdePrompt() {
return `目标项目已在微信开发者工具中打开,已跳过重复打开。按 ${RETRY_CONFIRM_KEYS.map((key) => colors.bold(colors.green(key))).join(" / ")} 关闭当前窗口后重新打开。`;
}
function disconnectMiniProgram(miniProgram) {
miniProgram.disconnect();
}
function withTimeout(task, timeoutMs) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(/* @__PURE__ */ new Error(`opened automator health check timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
return Promise.race([task, timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
async function verifyOpenedProjectHealth(miniProgram) {
if (typeof miniProgram.screenshot !== "function") return;
await withTimeout(miniProgram.screenshot({ timeout: OPENED_PROJECT_HEALTH_CHECK_TIMEOUT }), OPENED_PROJECT_HEALTH_CHECK_TIMEOUT);
}
async function openWechatIdeByAutomator(projectPath, options = {}) {
disconnectMiniProgram(await launchAutomator({
persistAsDefaultSession: true,
preserveProjectRoot: options.preserveProjectRoot ?? true,
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: OPEN_AUTOMATOR_TIMEOUT,
trustProject: options.trustProject !== false
}));
}
async function connectOpenedProject(projectPath) {
let miniProgram;
try {
miniProgram = await connectOpenedAutomator({
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: 3e3
});
} catch {
return { status: "not-connected" };
}
try {
await verifyOpenedProjectHealth(miniProgram);
return {
status: "connected",
miniProgram
};
} catch {
disconnectMiniProgram(miniProgram);
return { status: "unhealthy" };
}
}
/**
* @description 若当前项目已在微信开发者工具中打开且自动化可连通,则直接复用现有会话,避免重复拉起 IDE。
*/
async function tryReuseOpenedWechatIde(projectPath, closeIde, options = {}) {
const connection = await connectOpenedProject(projectPath);
if (connection.status === "unhealthy") return {
reopened: false,
reused: false,
unhealthy: true
};
if (connection.status === "not-connected") return null;
const { miniProgram } = connection;
disconnectMiniProgram(miniProgram);
if (options.promptReopen === false) {
logger_default.info("目标项目已在微信开发者工具中打开,已跳过重复打开。");
return {
reopened: false,
reused: true
};
}
logger_default.info(formatReuseOpenedWechatIdePrompt());
if (await promptRetryKeypress({ logger: logger_default }) !== "retry") return {
reopened: false,
reused: true
};
logger_default.info(colors.bold(colors.green("正在关闭当前已打开项目,并重新拉起微信开发者工具...")));
if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
await openWechatIdeByAutomator(projectPath, {
preserveProjectRoot: options.preserveProjectRoot,
trustProject: options.trustProject
});
return {
reopened: true,
reused: false
};
}
/**
* @description 对已打开的目标项目执行强制重开,以刷新最新构建产物。
*/
async function reopenOpenedWechatIde(projectPath, closeIde, options = {}) {
const connection = await connectOpenedProject(projectPath);
if (connection.status === "connected") {
disconnectMiniProgram(connection.miniProgram);
logger_default.info("目标项目已在微信开发者工具中打开,当前命令将主动重开以刷新最新构建产物。");
} else if (connection.status === "unhealthy") logger_default.info("目标项目的 automator 会话未通过健康检查,当前命令将关闭窗口并重新拉起。");
else logger_default.info("未检测到可复用的 automator 会话,当前命令将关闭现有窗口并重新拉起目标项目。");
if (!await (connection.status === "connected" ? closeIde : options.restartIde ?? closeIde)()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
await openWechatIdeByAutomator(projectPath, options);
return true;
}
//#endregion
//#region src/cli/openIde/index.ts
function shouldLogAutomatorFallbackError() {
const flag = process.env.WEAPP_VITE_DEBUG_AUTOMATOR_OPEN;
return flag === "1" || flag === "true";
}
const PREPARE_AUTOMATOR_SESSION_TIMEOUT = 8e3;
const AUTOMATOR_RECOVERY_QUIT_SETTLE_MS = 5e3;
const RESET_FILEUTILS_ENV = "WEAPP_VITE_RESET_IDE_FILEUTILS";
function sleep$1(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function readProjectConfigObject(projectPath) {
try {
const raw = fs.readFileSync(path.join(projectPath, "project.config.json"), "utf8");
const value = JSON.parse(raw);
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
} catch {
return;
}
}
function shouldUseAutomatorProjectWrapper(projectPath) {
const projectConfig = readProjectConfigObject(projectPath);
return projectConfig?.compileType === "plugin" && typeof projectConfig.miniprogramRoot === "string" && projectConfig.miniprogramRoot.trim().length > 0;
}
function isWechatIdeOpenRecoveryDisabled(options) {
if (options.openRecovery === false) return true;
const flag = process.env.WEAPP_VITE_DISABLE_IDE_OPEN_RECOVERY;
return flag === "1" || flag === "true";
}
function shouldResetWechatIdeFileUtils() {
const flag = process.env[RESET_FILEUTILS_ENV];
return flag === "1" || flag === "true";
}
/**
* @description 执行 IDE 打开流程,并在登录失效时允许按键重试。
*/
async function runWechatIdeOpenWithRetry(argv) {
await executeWechatIdeCliCommand(argv, {
cancelLevel: "warn",
onNonLoginError: (error) => logger_default.error(error),
onRetry: () => {
logger_default.info(colors.bold(colors.green("正在重试连接微信开发者工具...")));
},
promptOpenIdeLogin: true
});
}
/**
* @description 根据 mpDistRoot 推导 IDE 项目目录(目录内应包含 project/mini 配置)
*/
function resolveIdeProjectPath(mpDistRoot) {
if (!mpDistRoot || !mpDistRoot.trim()) return;
const parent = path.dirname(mpDistRoot);
if (!parent || parent === "." || parent === "/") return;
return parent;
}
/**
* @description 结合 mpDistRoot 与配置根目录解析最终 IDE 项目目录。
*/
function resolveIdeProjectRoot(mpDistRoot, cwd) {
return resolveIdeProjectPath(mpDistRoot) ?? cwd;
}
async function closeIde() {
return await closeIde$1();
}
async function restartWechatIdeForAutomatorRecovery() {
try {
await quitWechatIde();
await sleep$1(AUTOMATOR_RECOVERY_QUIT_SETTLE_MS);
return true;
} catch {
return await closeIde();
}
}
async function tryOpenWechatIdeByAutomator(projectPath, options) {
const preserveProjectRoot = !shouldUseAutomatorProjectWrapper(projectPath);
if (options.reuseOpenedProject === false) {
if (await reopenOpenedWechatIde(projectPath, closeIde, {
preserveProjectRoot,
restartIde: restartWechatIdeForAutomatorRecovery,
trustProject: options.trustProject
})) return "reopened";
}
const reuseResult = await tryReuseOpenedWechatIde(projectPath, closeIde, {
preserveProjectRoot,
promptReopen: options.reuseOpenedProject !== true,
trustProject: options.trustProject
});
if (reuseResult?.reused) return "reused";
if (reuseResult?.reopened) return "reopened";
if (reuseResult?.unhealthy) return "unhealthy";
await openWechatIdeByAutomator(projectPath, {
preserveProjectRoot,
trustProject: options.trustProject
});
return "opened";
}
/**
* @description 打开后主动刷新微信开发者工具的项目索引,避免模拟器沿用过期 app 配置。
*/
function appendLoginRetryArgv(argv, options) {
if (options.nonInteractive) argv.push("--non-interactive");
if (options.loginRetry) argv.push("--login-retry", options.loginRetry);
if (options.loginRetryTimeout) argv.push("--login-retry-timeout", options.loginRetryTimeout);
return argv;
}
function createIdeOpenArgv(platform, projectPath, options = {}) {
const argv = ["open", "-p"];
if (projectPath) argv.push(projectPath);
if (platform === "weapp" && options.trustProject !== false) argv.push("--trust-project");
if (platform && shouldPassPlatformArgToIdeOpen(platform)) argv.push("--platform", platform);
if (options.nonInteractive) argv.push("--non-interactive");
if (options.loginRetry) argv.push("--login-retry", options.loginRetry);
if (options.loginRetryTimeout) argv.push("--login-retry-timeout", options.loginRetryTimeout);
return argv;
}
async function prepareOpenedWechatIdeAutomatorSession(projectPath, options) {
const preserveProjectRoot = !shouldUseAutomatorProjectWrapper(projectPath);
try {
(await launchAutomator({
persistAsDefaultSession: true,
preserveProjectRoot,
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: PREPARE_AUTOMATOR_SESSION_TIMEOUT,
trustProject: options.trustProject !== false
})).disconnect?.();
return { ok: true };
} catch (error) {
logger_default.warn("准备当前项目的微信开发者工具自动化会话失败,截图、MCP 或 IDE 联动命令首次运行时将重新连接。");
logWechatIdeRecoveryHint({
projectPath,
reason: "无法建立当前项目的自动化会话,常见原因是 DevTools 服务端口未就绪、窗口停留在项目选择页,或存在残留 DevTools 会话。"
});
if (shouldLogAutomatorFallbackError()) logger_default.error(error);
return {
ok: false,
reason: "automator-session-failed",
error
};
}
}
async function connectOpenedWechatIdeAutomatorSession(projectPath) {
try {
(await connectOpenedAutomator({
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: PREPARE_AUTOMATOR_SESSION_TIMEOUT
})).disconnect?.();
return { ok: true };
} catch (error) {
logger_default.warn("连接当前项目的微信开发者工具自动化会话失败,截图、MCP 或 IDE 联动命令首次运行时将重新连接。");
logWechatIdeRecoveryHint({
projectPath,
reason: "当前项目已完成打开流程,但尚未连接到可复用的自动化会话。"
});
if (shouldLogAutomatorFallbackError()) logger_default.error(error);
return {
ok: false,
reason: "automator-session-failed",
error
};
}
}
async function prepareWechatIdeAutomatorSession(projectPath, options) {
if (options.prepareAutomatorSession === "connect-opened") return await connectOpenedWechatIdeAutomatorSession(projectPath);
return await prepareOpenedWechatIdeAutomatorSession(projectPath, options);
}
async function stabilizeOpenedWechatIdeProject(projectPath, servicePortEnabled, options = {}) {
if (servicePortEnabled === false) return {
ok: false,
reason: "service-port-disabled"
};
try {
if (shouldResetWechatIdeFileUtils()) await executeWechatIdeCliCommand(appendLoginRetryArgv([
"reset-fileutils",
"-p",
projectPath
], options), {
automatorMode: options.useAutomatorOpen === false ? "skip" : "prefer",
httpMode: "require",
onNonLoginError: (error) => logger_default.error(error),
preserveProjectRoot: options.useAutomatorOpen === false,
projectPath
});
try {
await executeWechatIdeCliCommand(appendLoginRetryArgv([
"engine",
"build",
projectPath
], options), {
automatorMode: options.useAutomatorOpen === false ? "skip" : "prefer",
engineBuildFallbackToCli: true,
httpMode: "prefer",
onNonLoginError: (error) => logger_default.error(error),
preserveProjectRoot: options.useAutomatorOpen === false,
projectPath
});
} catch (error) {
if (!isWechatIdeEngineBuildEndpointMissingError(error)) throw error;
logger_default.warn("当前微信开发者工具不支持自动 engine build 刷新,已跳过该步骤;如模拟器显示旧状态,可在开发者工具内手动编译。");
}
if (options.useAutomatorOpen !== false && options.skipAutomatorCompile !== true) try {
await executeWechatIdeCliCommand(appendLoginRetryArgv(["compile"], options), {
automatorMode: "require",
httpMode: "skip",
preserveProjectRoot: !shouldUseAutomatorProjectWrapper(projectPath),
projectPath
});
} catch (error) {
if (shouldLogAutomatorFallbackError()) logger_default.error(error);
}
return { ok: true };
} catch (error) {
if (isWechatIdeLoginRequiredExitError(error)) throw error;
logger_default.warn("刷新微信开发者工具项目索引失败,已保留当前打开状态;如模拟器仍显示旧状态,可手动刷新一次。");
logWechatIdeRecoveryHint({
projectPath,
reason: "打开项目后的文件索引刷新失败,DevTools 可能仍在使用旧项目状态或内部服务未就绪。"
});
if (shouldLogAutomatorFallbackError()) logger_default.error(error);
return {
ok: false,
reason: "index-refresh-failed",
error
};
}
}
async function verifyOpenedWechatIdeProject(projectPath, servicePortEnabled, options) {
const stabilizeResult = await stabilizeOpenedWechatIdeProject(projectPath, servicePortEnabled, options);
if (!stabilizeResult.ok) return stabilizeResult;
if (options.useAutomatorOpen === false && servicePortEnabled !== false) return await connectOpenedWechatIdeAutomatorSession(projectPath);
return stabilizeResult;
}
function formatWechatIdeOpenHealthReason(result) {
if (result.reason === "index-refresh-failed") return "项目索引刷新失败";
return "服务端口未开启";
}
async function recoverOpenedWechatIdeProject(platform, projectPath, servicePortEnabled, options, failedResult) {
if (failedResult.reason === "service-port-disabled") return failedResult;
if (failedResult.reason === "automator-session-failed") {
logger_default.warn("已跳过微信开发者工具自动恢复;自动化会话预热失败不影响当前项目打开,截图、MCP 或 IDE 联动命令首次运行时会重新连接。");
return failedResult;
}
if (isWechatIdeOpenRecoveryDisabled(options)) {
logger_default.warn("已跳过微信开发者工具自动恢复;请按上方提示手动关闭并重新打开目标项目。");
return failedResult;
}
logger_default.info(`检测到微信开发者工具打开后状态不稳定(${formatWechatIdeOpenHealthReason(failedResult)}),正在自动关闭并重新打开目标项目...`);
if (!await closeIde()) logger_default.warn("自动恢复时关闭当前微信开发者工具失败,仍继续尝试重新打开目标项目。");
await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, options));
const recoveredResult = await verifyOpenedWechatIdeProject(projectPath, servicePortEnabled, options);
if (recoveredResult.ok) logger_default.info("微信开发者工具已完成自动恢复。");
else logger_default.warn("微信开发者工具自动恢复未完成;可设置 `WEAPP_VITE_DISABLE_IDE_OPEN_RECOVERY=1` 或传入 `--no-open-recovery` 跳过自动关闭重开,并按提示手动处理。");
return recoveredResult;
}
async function verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, servicePortEnabled, options) {
const healthResult = await verifyOpenedWechatIdeProject(projectPath, servicePortEnabled, options);
if (!healthResult.ok) await recoverOpenedWechatIdeProject(platform, projectPath, servicePortEnabled, options, healthResult);
}
async function openIde(platform, projectPath, options = {}) {
let bootstrapResult;
const openStrategy = options.openStrategy ?? (options.useAutomatorOpen === true ? "automator" : "cli");
const useAutomatorOpen = openStrategy === "automator";
const normalizedOptions = {
...options,
openStrategy,
useAutomatorOpen
};
if (platform === "weapp" && projectPath) try {
bootstrapResult = await bootstrapWechatDevtoolsSettings({
projectPath,
trustProject: normalizedOptions.trustProject
});
} catch (error) {
logger_default.warn("检测微信开发者工具服务端口或写入项目信任状态失败,继续执行 open 流程。");
logger_default.error(error);
}
if (platform === "weapp" && projectPath && bootstrapResult?.servicePortEnabled === false) logWechatIdeServicePortDisabledHint(projectPath);
if (platform === "weapp" && projectPath && bootstrapResult?.servicePortEnabled !== false && useAutomatorOpen) try {
const openResult = await tryOpenWechatIdeByAutomator(projectPath, normalizedOptions);
if (openResult === "reused") return;
if (openResult === "unhealthy") {
logger_default.warn("已连接到目标微信开发者工具项目,但协议健康检查暂未就绪;已保留当前窗口并跳过重复启动。");
return;
} else if (openResult) {
if (!normalizedOptions.skipPostOpenHealthCheck) await verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, bootstrapResult?.servicePortEnabled, normalizedOptions);
return;
}
} catch (error) {
if (isAutomatorLoginError(error)) {
logger_default.error("检测到微信开发者工具登录状态失效,请先登录后重试。");
logger_default.warn(formatAutomatorLoginError(error));
}
logger_default.warn(`通过 automator 启动微信开发者工具失败,回退到普通 open 流程:${formatWechatIdeAutomatorFailure(error)}`);
if (shouldLogAutomatorFallbackError()) logger_default.error(error);
}
else if (platform === "weapp" && projectPath && normalizedOptions.reuseOpenedProject === false) {
if (!await closeIde()) logger_default.warn("关闭当前微信开发者工具失败,仍继续尝试打开目标项目。");
}
await runWechatIdeOpenWithRetry(createIdeOpenArgv(platform, projectPath, normalizedOptions));
if (platform === "weapp" && projectPath && normalizedOptions.prepareAutomatorSession) await prepareWechatIdeAutomatorSession(projectPath, normalizedOptions);
if (platform === "weapp" && projectPath && !normalizedOptions.skipPostOpenHealthCheck) await verifyAndRecoverOpenedWechatIdeProject(platform, projectPath, bootstrapResult?.servicePortEnabled, normalizedOptions);
}
/**
* @description 解析 IDE 相关命令所需的平台、项目目录与配置上下文。
*/
async function resolveIdeCommandContext(options) {
const cwd = options.cwd ?? process.cwd();
let platform = options.platform;
let projectPath = options.projectPath;
if (!platform || !projectPath) try {
const targets = resolveRuntimeTargets({ platform });
const ctx = await createCompilerContext({
cwd,
mode: options.mode ?? "development",
configFile: options.configFile,
inlineConfig: createInlineConfig(targets),
cliPlatform: options.cliPlatform
});
platform ??= ctx.configService.platform;
if (!projectPath) projectPath = resolveIdeProjectRoot(ctx.configService.mpDistRoot, ctx.configService.cwd);
return {
cwd: ctx.configService.cwd,
platform,
projectPath,
weappViteConfig: ctx.configService.weappViteConfig,
mpDistRoot: ctx.configService.mpDistRoot
};
} catch {}
if (!projectPath) {
const defaultProjectRoot = getDefaultIdeProjectRoot(platform);
if (defaultProjectRoot) projectPath = resolveIdeProjectRoot(defaultProjectRoot, cwd);
}
return {
cwd,
platform,
projectPath
};
}
//#endregion
//#region src/cli/options.ts
function filterDuplicateOptions(options) {
for (const [key, value] of Object.entries(options)) if (Array.isArray(value)) options[key] = value[value.length - 1];
}
function resolveConfigFile(options) {
if (typeof options.config === "string") return options.config;
if (typeof options.c === "string") return options.c;
}
function convertBase(value) {
if (value === 0) return "";
return value;
}
function coerceBooleanOption(value) {
if (value === void 0) return;
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (normalized === "") return true;
if (normalized === "false" || normalized === "0" || normalized === "off" || normalized === "no") return false;
if (normalized === "true" || normalized === "1" || normalized === "on" || normalized === "yes") return true;
return true;
}
if (typeof value === "number") return value !== 0;
return Boolean(value);
}
function isUiEnabled(options) {
return Boolean(options.ui || options.analyze);
}
//#endregion
//#region src/cli/commands/alipayExecute.ts
function createSpawnOptions() {
return {
shell: process.platform === "win32",
stdio: "inherit"
};
}
/**
* @description 执行本机 minidev 命令。
*/
async function runSpawnMinidev(command, argv, runner) {
await new Promise((resolve, reject) => {
const child = runner(command, argv, createSpawnOptions());
child.on("error", (error) => {
reject(error);
});
child.on("exit", (code, signal) => {
if (code === 0) {
resolve();
return;
}
reject(/* @__PURE__ */ new Error(signal ? `minidev ${argv[0] ?? ""} exited with signal ${signal}` : `minidev ${argv[0] ?? ""} exited with code ${code ?? "unknown"}`));
});
});
}
/**
* @description 使用系统进程执行本机 minidev 命令。
*/
async function spawnMinidev(command, argv) {
return await runSpawnMinidev(command, argv, spawn);
}
//#endregion
//#region src/cli/commands/alipay.ts
function normalizePassthroughArgs(args) {
return Array.isArray(args) ? args.filter((arg) => typeof arg === "string") : [];
}
function appendOption(argv, name, value) {
if (typeof value === "string" && value.trim()) argv.push(name, value);
}
function hasOption(argv, ...names) {
return argv.some((arg) => names.includes(arg) || names.some((name) => arg.startsWith(`${name}=`)));
}
function normalizeAlipayAction(action) {
if (action === "open") return "ide";
if (action === "ide" || action === "login" || action === "preview" || action === "upload") return action;
throw new Error(`未知 alipay 子命令: ${action ?? "(empty)"}`);
}
function resolveProjectPath(root, resolvedProjectPath, options) {
if (typeof options.project === "string" && options.project.trim()) return options.project;
return root ?? resolvedProjectPath;
}
function resolveMinidevCommand(options) {
return typeof options.minidev === "string" && options.minidev.trim() ? options.minidev : "minidev";
}
function createMinidevArgv(action, root, resolvedProjectPath, options) {
const passthroughArgs = normalizePassthroughArgs(options["--"]);
const argv = [action];
const projectPath = resolveProjectPath(root, resolvedProjectPath, options);
if ((action === "ide" || action === "preview" || action === "upload") && projectPath && !hasOption(passthroughArgs, "--project", "-p")) argv.push("--project", path.normalize(projectPath));
if ((action === "preview" || action === "upload") && !hasOption(passthroughArgs, "--app-id", "-a")) appendOption(argv, "--app-id", options.appId);
if ((action === "login" || action === "preview" || action === "upload") && !hasOption(passthroughArgs, "--client-type", "-c")) appendOption(argv, "--client-type", options.clientType);
if (action === "upload" && !hasOption(passthroughArgs, "--version", "-v")) appendOption(argv, "--version", options.version);
argv.push(...passthroughArgs);
return argv;
}
async function runAlipayCommand(action, root, options) {
const normalizedAction = normalizeAlipayAction(action);
filterDuplicateOptions(options);
const argv = createMinidevArgv(normalizedAction, root, (await resolveIdeCommandContext({
configFile: resolveConfigFile(options),
mode: options.mode ?? (normalizedAction === "upload" ? "production" : "development"),
platform: "alipay",
projectPath: root ?? options.project,
cliPlatform: "alipay"
})).projectPath, options);
const command = resolveMinidevCommand(options);
logger_default.info(`执行支付宝小程序 CLI:${command} ${argv.join(" ")}`);
await spawnMinidev(command, argv);
}
function registerAlipayCommand(cli) {
cli.command("alipay [action] [root]", "run Alipay minidev ide, login, preview, or upload").option("-a, --app-id <appId>", "[string] Alipay mini program appId").option("-c, --client-type <clientType>", "[string] minidev client type").option("--minidev <command>", "[string] minidev executable path or command name").option("--project <path>", "[string] Alipay mini program project path").option("--version <version>", "[string] upload version").allowUnknownOptions().action(async (action, root, options) => {
await runAlipayCommand(action, root, options);
});
}
//#endregion
//#region src/analyze/hmr.ts
function createMetricSummary(values) {
if (!values.length) return { count: 0 };
const total = values.reduce((sum, value) => sum + value, 0);
return {
count: values.length,
averageMs: total / values.length,
maxMs: Math.max(...values)
};
}
function createOperationSummary(values) {
if (!values.length) return { count: 0 };
const total = values.reduce((sum, value) => sum + value, 0);
return {
count: values.length,
average: total / values.length,
max: Math.max(...values)
};
}
function sortCountEntries(map) {
return [...map.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([name, count]) => ({
name,
count
}));
}
function collectCounts(target, values) {
for (const value of values ?? []) {
if (!value) continue;
target.set(value, (target.get(value) ?? 0) + 1);
}
}
function isFiniteNumber$1(value) {
return typeof value === "number" && Number.isFinite(value);
}
/**
* @description 聚合 HMR JSONL profile,为命令行与后续仪表盘复用。
*/
async function analyzeHmrProfile(options) {
const lines = (await fs$1.readFile(options.profilePath, "utf8")).split(/\r?\n/);
const samples = [];
let skippedLineCount = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (!isFiniteNumber$1(parsed.totalMs)) {
skippedLineCount += 1;
continue;
}
samples.push(parsed);
} catch {
skippedLineCount += 1;
}
}
const eventCounts = /* @__PURE__ */ new Map();
const dirtyReasonCounts = /* @__PURE__ */ new Map();
const pendingReasonCounts = /* @__PURE__ */ new Map();
const totalValues = [];
const buildCoreValues = [];
const buildStartValues = [];
const pluginResolveValues = [];
const transformValues = [];
const coreTransformValues = [];
const wevuTransformValues = [];
const vueTransformValues = [];
const vueReadSourceValues = [];
const vueCompileValues = [];
const vueFinalizeCompiledValues = [];
const vueFinalizeCodeValues = [];
const coreLoadValues = [];
const entryLoadValues = [];
const entryCodeReadValues = [];
const entrySidecarResolveValues = [];
const entryJsonReadValues = [];
const entryVueConfigValues = [];
const entryTemplateScanValues = [];
const entryScriptSetupValues = [];
const entryVueSignatureValues = [];
const entryAutoImportValues = [];
const entryPrepareValues = [];
const entryEmitOutputValues = [];
const entryStyleScanValues = [];
const entryStyleReadValues = [];
const entryResolveValues = [];
const entryChunkEmitValues = [];
const entryChunkLoadValues = [];
const entryChunkEmitFileValues = [];
const entryLayoutValues = [];
const requestGlobalsValues = [];
const weapiResolveValues = [];
const renderStartValues = [];
const generateBundleValues = [];
const generateSharedValues = [];
const generateRewriteValues = [];
const generateModuleGraphValues = [];
const snapshotResolveValues = [];
const snapshotBuildValues = [];
const writeValues = [];
const watchToDirtyValues = [];
const emitValues = [];
const sharedChunkValues = [];
const chunkEmitCountValues = [];
const loadCountValues = [];
const resolveCountValues = [];
const skippedLoadedCountValues = [];
for (const sample of samples) {
totalValues.push(sample.totalMs);
if (sample.event) eventCounts.set(sample.event, (eventCounts.get(sample.event) ?? 0) + 1);
if (isFiniteNumber$1(sample.buildCoreMs)) buildCoreValues.push(sample.buildCoreMs);
if (isFiniteNumber$1(sample.buildStartMs)) buildStartValues.push(sample.buildStartMs);
if (isFiniteNumber$1(sample.pluginResolveMs)) pluginResolveValues.push(sample.pluginResolveMs);
if (isFiniteNumber$1(sample.transformMs)) transformValues.push(sample.transformMs);
if (isFiniteNumber$1(sample.coreTransformMs)) coreTransformValues.push(sample.coreTransformMs);
if (isFiniteNumber$1(sample.wevuTransformMs)) wevuTransformValues.push(sample.wevuTransformMs);
if (isFiniteNumber$1(sample.vueTransformMs)) vueTransformValues.push(sample.vueTransformMs);
if (isFiniteNumber$1(sample.vueReadSourceMs)) vueReadSourceValues.push(sample.vueReadSourceMs);
if (isFiniteNumber$1(sample.vueCompileMs)) vueCompileValues.push(sample.vueCompileMs);
if (isFiniteNumber$1(sample.vueFinalizeCompiledMs)) vueFinalizeCompiledValues.push(sample.vueFinalizeCompiledMs);
if (isFiniteNumber$1(sample.vueFinalizeCodeMs)) vueFinalizeCodeValues.push(sample.vueFinalizeCodeMs);
if (isFiniteNumber$1(sample.coreLoadMs)) coreLoadValues.push(sample.coreLoadMs);
if (isFiniteNumber$1(sample.entryLoadMs)) entryLoadValues.push(sample.entryLoadMs);
if (isFiniteNumber$1(sample.entryCodeReadMs)) entryCodeReadValues.push(sample.entryCodeReadMs);
if (isFiniteNumber$1(sample.entrySidecarResolveMs)) entrySidecarResolveValues.push(sample.entrySidecarResolveMs);
if (isFiniteNumber$1(sample.entryJsonReadMs)) entryJsonReadValues.push(sample.entryJsonReadMs);
if (isFiniteNumber$1(sample.entryVueConfigMs)) entryVueConfigValues.push(sample.entryVueConfigMs);
if (isFiniteNumber$1(sample.entryTemplateScanMs)) entryTemplateScanValues.push(sample.entryTemplateScanMs);
if (isFiniteNumber$1(sample.entryScriptSetupMs)) entryScriptSetupValues.push(sample.entryScriptSetupMs);
if (isFiniteNumber$1(sample.entryVueSignatureMs)) entryVueSignatureValues.push(sample.entryVueSignatureMs);
if (isFiniteNumber$1(sample.entryAutoImportMs)) entryAutoImportValues.push(sample.entryAutoImportMs);
if (isFiniteNumber$1(sample.entryPrepareMs)) entryPrepareValues.push(sample.entryPrepareMs);
if (isFiniteNumber$1(sample.entryEmitOutputMs)) entryEmitOutputValues.push(sample.entryEmitOutputMs);
if (isFiniteNumber$1(sample.entryStyleScanMs)) entryStyleScanValues.push(sample.entryStyleScanMs);
if (isFiniteNumber$1(sample.entryStyleReadMs)) entryStyleReadValues.push(sample.entryStyleReadMs);
if (isFiniteNumber$1(sample.entryResolveMs)) entryResolveValues.push(sample.entryResolveMs);
if (isFiniteNumber$1(sample.entryChunkEmitMs)) entryChunkEmitValues.push(sample.entryChunkEmitMs);
if (isFiniteNumber$1(sample.entryChunkLoadMs)) entryChunkLoadValues.push(sample.entryChunkLoadMs);
if (isFiniteNumber$1(sample.entryChunkEmitFileMs)) entryChunkEmitFileValues.push(sample.entryChunkEmitFileMs);
if (isFiniteNumber$1(sample.entryLayoutMs)) entryLayoutValues.push(sample.entryLayoutMs);
if (isFiniteNumber$1(sample.requestGlobalsMs)) requestGlobalsValues.push(sample.requestGlobalsMs);
if (isFiniteNumber$1(sample.weapiResolveMs)) weapiResolveValues.push(sample.weapiResolveMs);
if (isFiniteNumber$1(sample.renderStartMs)) renderStartValues.push(sample.renderStartMs);
if (isFiniteNumber$1(sample.generateBundleMs)) generateBundleValues.push(sample.generateBundleMs);
if (isFiniteNumber$1(sample.generateSharedMs)) generateSharedValues.push(sample.generateSharedMs);
if (isFiniteNumber$1(sample.generateRewriteMs)) generateRewriteValues.push(sample.generateRewriteMs);
if (isFiniteNumber$1(sample.generateModuleGraphMs)) generateModuleGraphValues.push(sample.generateModuleGraphMs);
if (isFiniteNumber$1(sample.snapshotResolveMs)) snapshotResolveValues.push(sample.snapshotResolveMs);
if (isFiniteNumber$1(sample.snapshotBuildMs)) snapshotBuildValues.push(sample.snapshotBuildMs);
if (isFiniteNumber$1(sample.writeMs)) writeValues.push(sample.writeMs);
if (isFiniteNumber$1(sample.watchToDirtyMs)) watchToDirtyValues.push(sample.watchToDirtyMs);
if (isFiniteNumber$1(sample.emitMs)) emitValues.push(sample.emitMs);
if (isFiniteNumber$1(sample.sharedChunkResolveMs)) sharedChunkValues.push(sample.sharedChunkResolveMs);
if (isFiniteNumber$1(sample.chunkEmitCount)) chunkEmitCountValues.push(sample.chunkEmitCount);
if (isFiniteNumber$1(sample.loadCount)) loadCountValues.push(sample.loadCount);
if (isFiniteNumber$1(sample.resolveCount)) resolveCountValues.push(sample.resolveCount);
if (isFiniteNumber$1(sample.skippedLoadedCount)) skippedLoadedCountValues.push(sample.skippedLoadedCount);
collectCounts(dirtyReasonCounts, sample.dirtyReasonSummary);
collectCounts(pendingReasonCounts, sample.pendingReasonSummary);
}
const orderedByTime = [...samples].sort((left, right) => {
const leftTime = typeof left.timestamp === "string" ? Date.parse(left.timestamp) : NaN;
const rightTime = typeof right.timestamp === "string" ? Date.parse(right.timestamp) : NaN;
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) return leftTime - rightTime;
return 0;
});
const slowestSamples = [...samples].sort((left, right) => (right.totalMs ?? 0) - (left.totalMs ?? 0)).slice(0, options.topSlowest ?? 5);
return {
runtime: "mini",
kind: "hmr-profile",
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
profilePath: options.profilePath,
sampleCount: samples.length,
skippedLineCount,
firstTimestamp: orderedByTime[0]?.timestamp,
lastTimestamp: orderedByTime[orderedByTime.length - 1]?.timestamp,
metrics: {
totalMs: createMetricSummary(totalValues),
buildCoreMs: createMetricSummary(buildCoreValues),
buildStartMs: createMetricSummary(buildStartValues),
pluginResolveMs: createMetricSummary(pluginResolveValues),
transformMs: createMetricSummary(transformValues),
coreTransformMs: createMetricSummary(coreTransformValues),
wevuTransformMs: createMetricSummary(wevuTransformValues),
vueTransformMs: createMetricSummary(vueTransformValues),
vueReadSourceMs: createMetricSummary(vueReadSourceValues),
vueCompileMs: createMetricSummary(vueCompileValues),
vueFinalizeCompiledMs: createMetricSummary(vueFinalizeCompiledValues),
vueFinalizeCodeMs: createMetricSummary(vueFinalizeCodeValues),
coreLoadMs: createMetricSummary(coreLoadValues),
entryLoadMs: createMetricSummary(entryLoadValues),
entryCodeReadMs: createMetricSummary(entryCodeReadValues),
entrySidecarResolveMs: createMetricSummary(entrySidecarResolveValues),
entryJsonReadMs: createMetricSummary(entryJsonReadValues),
entryVueConfigMs: createMetricSummary(entryVueConfigValues),
entryTemplateScanMs: createMetricSummary(entryTemplateScanValues),
entryScriptSetupMs: createMetricSummary(entryScriptSetupValues),
entryVueSignatureMs: createMetricSummary(entryVueSignatureValues),
entryAutoImportMs: createMetricSummary(entryAutoImportValues),
entryPrepareMs: createMetricSummary(entryPrepareValues),
entryEmitOutputMs: createMetricSummary(entryEmitOutputValues),
entryStyleScanMs: createMetricSummary(entryStyleScanValues),
entryStyleReadMs: createMetricSummary(entryStyleReadValues),
entryResolveMs: createMetricSummary(entryResolveValues),
entryChunkEmitMs: createMetricSummary(entryChunkEmitValues),
entryChunkLoadMs: createMetricSummary(entryChunkLoadValues),
entryChunkEmitFileMs: createMetricSummary(entryChunkEmitFileValues),
entryLayoutMs: createMetricSummary(entryLayoutValues),
requestGlobalsMs: createMetricSummary(requestGlobalsValues),
weapiResolveMs: createMetricSummary(weapiResolveValues),
renderStartMs: createMetricSummary(renderStartValues),
generateBundleMs: createMetricSummary(generateBundleValues),
generateSharedMs: createMetricSummary(generateSharedValues),
generateRewriteMs: createMetricSummary(generateRewriteValues),
generateModuleGraphMs: createMetricSummary(generateModuleGraphValues),
snapshotResolveMs: createMetricSummary(snapshotResolveValues),
snapshotBuildMs: createMetricSummary(snapshotBuildValues),
writeMs: createMetricSummary(writeValues),
watchToDirtyMs: createMetricSummary(watchToDirtyValues),
emitMs: createMetricSummary(emitValues),
sharedChunkResolveMs: createMetricSummary(sharedChunkValues)
},
operations: {
chunkEmitCount: createOperationSummary(chunkEmitCountValues),
loadCount: createOperationSummary(loadCountValues),
resolveCount: createOperationSummary(resolveCountValues),
skippedLoadedCount: createOperationSummary(skippedLoadedCountValues)
},
events: sortCountEntries(eventCounts),
dirtyReasons: sortCountEntries(dirtyReasonCounts),
pendingReasons: sortCountEntries(pendingReasonCounts),
slowestSamples
};
}
//#endregion
//#region src/analyze/preload/budget.ts
const PRELOAD_LIMIT_BYTES = 2097152;
function createPackageSizeMap$1(result) {
return new Map((result?.packages ?? []).map((pkg) => [pkg.id, pkg.files.reduce((total, file) => total + (file.size ?? 0), 0)]));
}
function getConfiguredPackages(rule, appJson) {
if (!rule || typeof rule !== "object" || !("packages" in rule)) return [];
const packages = rule.packages;
if (!Array.isArray(packages)) return [];
return packages.flatMap((item) => typeof item === "string" ? [resolvePreloadPackageIdentifier(item, appJson)] : []).filter((item) => Boolean(item));
}
function createPreloadBudgets(appJson, result, configuredRules, packageAnalysis) {
const packageSizes = createPackageSizeMap$1(packageAnalysis);
const bySourcePackage = /* @__PURE__ */ new Map();
const ensureSourcePackage = (page) => {
const sourcePackage = page.packageRoot ?? "__main__";
let entry = bySourcePackage.get(sourcePackage);
if (!entry) {
entry = {
sourceType: page.packageRoot ? page.independent ? "independent" : "subpackage" : "main",
targets: /* @__PURE__ */ new Map()
};
bySourcePackage.set(sourcePackage, entry);
}
return entry;
};
for (const page of result.pages) {
const configuredKey = findPreloadRuleKey(page.route, configuredRules);
if (configuredKey === void 0) continue;
const entry = ensureSourcePackage(page);
for (const packageRoot of getConfiguredPackages(configuredRules[configuredKey], appJson)) {
const target = entry.targets.get(packageRoot) ?? {
configured: false,
suggested: false
};
target.configured = true;
entry.targets.set(packageRoot, target);
}
}
const pageByRoute = new Map(result.pages.map((page) => [page.route, page]));
for (const suggestion of result.suggestions) {
const page = pageByRoute.get(suggestion.page);
if (!page) continue;
const entry = ensureSourcePackage(page);
const target = entry.targets.get(suggestion.packageRoot) ?? {
configured: false,
suggested: false
};
target.suggested = true;
entry.targets.set(suggestion.packageRoot, target);
}
return [...bySourcePackage.entries()].map(([sourcePackage, entry]) => {
const targets = [...entry.targets.entries()].map(([packageRoot, state]) => {
const packageId = packageRoot === "__APP__" ? "__main__" : packageRoot;
return {
packageRoot,
bytes: packageSizes.get(packageId),
...state
};
}).sort((left, right) => left.packageRoot.localeCompare(right.packageRoot));
const unknownPackages = targets.filter((target) => target.bytes === void 0).map((target) => target.packageRoot);
const estimatedBytes = targets.reduce((total, target) => total + (target.bytes ?? 0), 0);
const status = estimatedBytes > PRELOAD_LIMIT_BYTES ? "exceeded" : unknownPackages.length > 0 ? "unknown" : "ok";
return {
sourcePackage,
sourceType: entry.sourceType,
limitBytes: PRELOAD_LIMIT_BYTES,
estimatedBytes,
remainingBytes: unknownPackages.length === 0 ? Math.max(0, PRELOAD_LIMIT_BYTES - estimatedBytes) : void 0,
status,
targets,
unknownPackages
};
}).sort((left, right) => left.sourcePackage.localeCompare(right.sourcePackage));
}
//#endregion
//#region src/analyze/preload/index.ts
async function readFileIfExists(filePath) {
if (!filePath) return;
try {
return await fs$1.readFile(filePath, "utf8");
} catch {
return;
}
}
function splitVueSource(source, filename) {
try {
const { descriptor } = parse(source, { filename });
return {
template: descriptor.template?.content,
script: [descriptor.script?.content, descriptor.scriptSetup?.content].filter((content) => Boolean(content)).join("\n")
};
} catch {
return {
template: source,
script: source
};
}
}
async function collectPageSources(ctx, route) {
const base = path.resolve(ctx.configService.absoluteSrcRoot, route);
const [templateEntry, scriptEntry, vueEntry] = await Promise.all([
findTemplateEntry(base, ctx.configService.platform),
findJsEntry(base),
findVueEntry(base)
]);
if (vueEntry) {
const source = await readFileIfExists(vueEntry);
return source ? splitVueSource(source, vueEntry) : {};
}
const [template, script] = await Promise.all([readFileIfExists(templateEntry.path), readFileIfExists(scriptEntry.path)]);
return {
template,
script
};
}
function aggregateSuggestions(result, configuredRules, appJson) {
const byPage = /* @__PURE__ */ new Map();
for (const suggestion of result.suggestions) {
const current = byPage.get(suggestion.page) ?? {
page: suggestion.page,
packages: [],
evidence: [],
alreadyConfigured: []
};
if (!current.packages.includes(suggestion.packageRoot)) current.packages.push(suggestion.packageRoot);
current.evidence.push({
target: suggestion.target,
packageRoot: suggestion.packageRoot,
source: suggestion.source
});
const configuredKey = findPreloadRuleKey(suggestion.page, configuredRules);
const existingRule = configuredKey === void 0 ? void 0 : configuredRules[configuredKey];
const existingPackages = existingRule && typeof existingRule === "object" && "packages" in existingRule ? existingRule.packages : void 0;
if ((Array.isArray(existingPackages) ? existingPackages.flatMap((item) => typeof item === "string" ? [resolvePreloadPackageIdentifier(item, appJson)] : []) : []).includes(suggestion.packageRoot)) current.alreadyConfigured.push(suggestion.packageRoot);
byPage.set(suggestion.page, current);
}
return [...byPage.values()].map((suggestion) => ({
...suggestion,
packages: suggestion.packages.sort(),
alreadyConfigured: [...new Set(suggestion.alreadyConfigured)].sort(),
evidence: suggestion.evidence.sort((left, right) => left.target.localeCompare(right.target))
}));
}
async function analyzePreloadRules(ctx, options = {}) {
const appJson = (await ctx.scanService.loadAppEntry()).json;
const pages = collectPreloadPages(appJson);
const pageSources = /* @__PURE__ */ new Map();
const sourceEntries = await Promise.all(pages.map(async (page) => [page.route, await collectPageSources(ctx, page.route)]));
for (const [route, source] of sourceEntries) pageSources.set(route, source);
const configuredRules = appJson.preloadRule && typeof appJson.preloadRule === "object" ? appJson.preloadRule : {};
const suggestions = suggestPreloadRules(appJson, pageSources);
return {
runtime: "mini",
kind: "preload",
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
platform: ctx.configService.platform,
pages: pages.map((page) => page.route),
configuredRules,
suggestions: aggregateSuggestions(suggestions, configuredRules, appJson),
budgets: createPreloadBudgets(appJson, suggestions, configuredRules, options.packageAnalysis),
uncoveredPages: suggestions.uncovered,
limitations: [
"仅分析静态路由字面量;动态路由、后端配置和运行时守卫不会被推断。",
"建议使用分包 root 作为 packages 值;预算按触发页所属包聚合,未知体积目标仍需人工复核。",
"静态跳转只能证明可达性,不能代表真实访问频率,正式启用前仍需结合业务数据确认。",
"该命令只提供可审计建议,不会自动修改源码或覆盖手写的 app.json.preloadRule。"
]
};
}
//#endregion
//#region src/analyze/components/suggestions.ts
function createComponentSuggestion(usage) {
if (usage.componentPackage !== "__main__" || usage.crossPackageUsageCount === 0) return;
if (usage.crossPackageUsageCount === usage.placeholderCoveredCrossPackageUsageCount) return;
const pagePackages = Array.from(new Set(Array.from(usage.pages.values()).map((page) => page.packageId))).sort((left, right) => {
if (left === "__main__") return -1;
if (right === "__main__") return 1;
return left.localeCompare(right);
});
const subPackageIds = pagePackages.filter((packageId) => packageId !== "__main__");
const usedByMain = pagePackages.includes("__main__");
if (subPackageIds.length === 1 && !usedByMain) {
const targetPackage = subPackageIds[0];
return {
kind: "move-to-subpackage",
component: usage.component,
componentPackage: usage.componentPackage,
targetPackage,
pagePackages,
message: `主包组件 ${usage.component} 仅被分包 ${targetPackage} 使用,建议评估移动到该分包。`
};
}
if (subPackageIds.length > 1) return {
kind: "shared-subpackage-or-placeholder",
component: usage.component,
componentPackage: usage.componentPackage,
pagePackages,
message: `主包组件 ${usage.component} 被多个分包使用,建议评估分包归属、共享策略或 componentPlaceholder。`
};
if (usedByMain && subPackageIds.length > 0) return {
kind: "split-or-async",
component: usage.component,
componentPackage: usage.componentPackage,
pagePackages,
message: `主包组件 ${usage.component} 同时被主包和分包使用,建议评估组件拆分、归属或异步化策略。`
};
}
//#endregion
//#region src/analyze/components/index.ts
function normalizeRoute(value) {
return posix.normalize(value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.json$/, ""));
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function toStringArray(value) {
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim() !== "") : [];
}
function readUsingComponents(config) {
if (!isRecord(config) || !isRecord(config.usingComponents)) return [];
return Object.entries(config.usingComponents).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "");
}
function readComponentGenericDefaults(config) {
if (!isRecord(config) || !isRecord(config.componentGenerics)) return [];
return Object.entries(config.componentGenerics).map(([name, value]) => isRecord(value) && typeof value.default === "string" && value.default.trim() !== "" ? [name, value.default] : void 0).filter((entry) => Boolean(entry));
}
function readComponentPlaceholder(config) {
if (!isRecord(config) || !isRecord(config.componentPlaceholder)) return /* @__PURE__ */ new Set();
return new Set(Object.keys(config.componentPlaceholder));
}
function resolveComponentRoute(owner, request) {
const normalizedRequest = request.replace(/\\/g, "/").trim();
if (!normalizedRequest || normalizedRequest.startsWith("plugin://")) return;
if (normalizedRequest.startsWith(".")) return normalizeRoute(posix.join(posix.dirname(owner), normalizedRequest));
if (normalizedRequest.startsWith("/")) return normalizeRoute(normalizedRequest);
return normalizeRoute(normalizedRequest);
}
function resolvePackageId(route, subPackages) {
return subPackages.map((item) => item.root).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => route === root || route.startsWith(`${root}/`)) ?? "__main__";
}
function collectAppPages(configs) {
const appJson = configs.get("app");
if (!isRecord(appJson)) return /* @__PURE__ */ new Set();
const pages = /* @__PURE__ */ new Set();
for (const page of toStringArray(appJson.pages)) pages.add(normalizeRoute(page));
const subPackages = Array.isArray(appJson.subPackages) ? appJson.subPackages : Array.isArray(appJson.subpackages) ? appJson.subpackages : [];
for (const item of subPackages) {
if (!isRecord(item) || typeof item.root !== "string") continue;
for (const page of toStringArray(item.pages)) pages.add(normalizeRoute(posix.join(item.root, page)));
}
return pages;
}
function registerUsage(usageMap, edge, page, pagePackage, componentPackage) {
const usage = usageMap.get(edge.component) ?? {
component: edge.component,
componentPackage,
totalUsageCount: 0,
pages: /* @__PURE__ */ new Map(),
crossPackageUsageCount: 0,
placeholderCoveredCrossPackageUsageCount: 0
};
usage.totalUsageCount += 1;
const pageUsage = usage.pages.get(page) ?? {
page,
packageId: pagePackage,
usageCount: 0
};
pageUsage.usageCount += 1;
usage.pages.set(page, pageUsage);
if (pagePackage !== componentPackage) {
usage.crossPackageUsageCount += 1;
if (edge.placeholderCovered) usage.placeholderCoveredCrossPackageUsageCount += 1;
}
usageMap.set(edge.component, usage);
}
function collectAnalyzeComponentJsonConfigs(output) {
if (!output) return [];
const configs = [];
for (const item of output.output ?? []) {
if (item.type !== "asset" || !item.fileName.endsWith(".json")) continue;
const asset = item;
if (typeof asset.source !== "string") continue;
try {
configs.push({
file: normalizeRoute(asset.fileName),
config: JSON.parse(asset.source)
});
} catch {}
}
return configs;
}
function analyzeComponentUsage(options) {
const configs = /* @__PURE__ */ new Map();
for (const item of options.jsonConfigs) configs.set(normalizeRoute(item.file), item.config);
const pages = collectAppPages(configs);
const graph = /* @__PURE__ */ new Map();
const packageMap = /* @__PURE__ */ new Map();
for (const route of configs.keys()) packageMap.set(route, resolvePackageId(route, options.subPackages));
for (const [owner, config] of configs) {
const placeholders = readComponentPlaceholder(config);
const edges = [...readUsingComponents(config), ...readComponentGenericDefaults(config)].map(([name, request]) => {
const component = resolveComponentRoute(owner, request);
return component && configs.has(component) ? {
owner,
component,
placeholderCovered: placeholders.has(name)
} : void 0;
}).filter((edge) => Boolean(edge)).filter((edge, index, all) => all.findIndex((item) => item.component === edge.component) === index);
if (edges.length > 0) graph.set(owner, edges);
}
const usageMap = /* @__PURE__ */ new Map();
const visit = (owner, page, stack) => {
for (const edge of graph.get(owner) ?? []) {
const componentPackage = packageMap.get(edge.component) ?? "__main__";
const pagePackage = packageMap.get(page) ?? "__main__";
registerUsage(usageMap, edge, page, pagePackage, componentPackage);
if (stack.has(edge.component)) continue;
const nextStack = new Set(stack);
nextStack.add(edge.component);
visit(edge.component, page, nextStack);
}
};
for (const page of pages) visit(page, page, /* @__PURE__ */ new Set([page]));
return Array.from(usageMap.values()).map((usage) => {
const pages = Array.from(usage.pages.values()).sort((left, right) => left.page.localeCompare(right.page));
const suggestions = [createComponentSuggestion(usage)].filter((item) => Boolean(item));
return {
component: usage.component,
componentPackage: usage.componentPackage,
totalUsageCount: usage.totalUsageCount,
pageUsageCount: pages.length,
pages,
suggestions
};
}).sort((left, right) => {
const usageDelta = right.totalUsageCount - left.totalUsageCount;
return usageDelta !== 0 ? usageDelta : left.component.localeCompare(right.component);
});
}
//#endregion
//#region src/analyze/subpackages/metadata.ts
const defaultTotalBudgetBytes = 20971520;
const defaultPackageBudgetBytes = 2097152;
const defaultWarningRatio = .85;
const defaultHistoryDir = ".weapp-vite/analyze-history";
const defaultHistoryLimit = 20;
function resolveBudgetValue(value, fallback) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}
function resolveHistoryLimit(value) {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : defaultHistoryLimit;
}
function resolveAnalyzeBudgets(configService) {
const budgets = configService.weappViteConfig.analyze?.budgets;
const legacyPackageBudget = configService.weappViteConfig.packageSizeWarningBytes;
const packageFallback = resolveBudgetValue(legacyPackageBudget, defaultPackageBudgetBytes);
return {
totalBytes: resolveBudgetValue(budgets?.totalBytes, defaultTotalBudgetBytes),
mainBytes: resolveBudgetValue(budgets?.mainBytes, packageFallback),
subPackageBytes: resolveBudgetValue(budgets?.subPackageBytes, packageFallback),
independentBytes: resolveBudgetValue(budgets?.independentBytes, packageFallback),
warningRatio: resolveBudgetValue(budgets?.warningRatio, defaultWarningRatio),
source: budgets ? "config" : "default"
};
}
function resolveAnalyzeHistoryMetadata(configService) {
const history = configService.weappViteConfig.analyze?.history;
if (history === false) return {
enabled: false,
dir: path.resolve(configService.cwd, defaultHistoryDir),
limit: defaultHistoryLimit
};
const historyConfig = typeof history === "object" && history ? history : {};
const rawDir = typeof historyConfig.dir === "string" && historyConfig.dir.trim() ? historyConfig.dir.trim() : defaultHistoryDir;
return {
enabled: historyConfig.enabled !== false,
dir: path.isAbsolute(rawDir) ? rawDir : path.resolve(configService.cwd, rawDir),
limit: resolveHistoryLimit(historyConfig.limit)
};
}
function createAnalyzeMetadata(configService, now = /* @__PURE__ */ new Date()) {
const history = resolveAnalyzeHistoryMetadata(configService);
return {
generatedAt: now.toISOString(),
budgets: resolveAnalyzeBudgets(configService),
history: {
...history,
dir: configService.relativeCwd(history.dir)
}
};
}
//#endregion
//#region src/analyze/subpackages/classifier.ts
const VIRTUAL_MODULE_INDICATOR = "\0";
const VIRTUAL_PREFIX = `${SHARED_CHUNK_VIRTUAL_PREFIX}/`;
function classifyModuleSourceKind(options) {
if (options.isNodeModule) return "node_modules";
if (options.inSrc) return "src";
if (options.inPlugin) return "plugin";
return "workspace";
}
function resolvePluginAssetAbsolute(normalizedFileName, pluginRoot) {
if (!pluginRoot) return;
const pluginBase = posix.basename(pluginRoot);
if (normalizedFileName !== pluginBase && !normalizedFileName.startsWith(`${pluginBase}/`)) return;
const relative = normalizedFileName === pluginBase ? "" : normalizedFileName.slice(pluginBase.length + 1);
const absolute = posix.resolve(pluginRoot, relative);
return isPathInside(pluginRoot, absolute) ? absolute : void 0;
}
function resolveSubPackageRoot$1(fileName, context) {
const normalized = posix.normalize(fileName);
return Array.from(context.subPackageRoots).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => normalized === root || normalized.startsWith(`${root}/`));
}
function classifyPackage(fileName, origin, context) {
if (fileName.startsWith(VIRTUAL_PREFIX)) {
const combination = fileName.slice(VIRTUAL_PREFIX.length).split("/")[0] || "shared";
return {
id: `virtual:${combination}`,
label: `共享虚拟包 ${combination}`,
type: "virtual"
};
}
const rootCandidate = resolveSubPackageRoot$1(fileName, context);
if (rootCandidate) {
const isIndependent = context.independentRoots.has(rootCandidate);
return {
id: rootCandidate,
label: `${isIndependent ? "独立分包" : "分包"} ${rootCandidate}`,
type: isIndependent || origin === "independent" ? "independent" : "subPackage"
};
}
return {
id: "__main__",
label: "主包",
type: "main"
};
}
function normalizeModuleId(id) {
if (!id || id.includes(VIRTUAL_MODULE_INDICATOR)) return;
if (!posix.isAbsolute(id)) return;
return posix.normalize(id);
}
function resolveModuleSourceType(absoluteId, ctx) {
const { configService } = ctx;
const isNodeModule = absoluteId.includes("/node_modules/") || absoluteId.includes("\\node_modules\\");
const pluginRoot = configService.absolutePluginRoot;
const srcRoot = configService.absoluteSrcRoot;
const sourceType = classifyModuleSourceKind({
isNodeModule,
inSrc: isPathInside(srcRoot, absoluteId),
inPlugin: pluginRoot ? isPathInside(pluginRoot, absoluteId) : false
});
return {
source: configService.relativeAbsoluteSrcRoot(absoluteId),
sourceType
};
}
function resolveAssetSource(fileName, ctx) {
const { configService } = ctx;
const normalized = posix.normalize(fileName);
const srcCandidate = posix.resolve(configService.absoluteSrcRoot, normalized);
if (isPathInside(configService.absoluteSrcRoot, srcCandidate)) return {
absolute: srcCandidate,
source: configService.relativeAbsoluteSrcRoot(srcCandidate),
sourceType: "src"
};
const pluginAbsolute = resolvePluginAssetAbsolute(normalized, configService.absolutePluginRoot);
if (pluginAbsolute) return {
absolute: pluginAbsolute,
source: configService.relativeAbsoluteSrcRoot(pluginAbsolute),
sourceType: "plugin"
};
}
//#endregion
//#region src/analyze/subpackages/registry.ts
function ensurePackage(packages, classification) {
const existing = packages.get(classification.id);
if (existing) return existing;
const created = {
...classification,
files: /* @__PURE__ */ new Map()
};
packages.set(classification.id, created);
return created;
}
function ensureModule(modules, id, source, sourceType) {
const existing = modules.get(id);
if (existing) return existing;
const created = {
id,
source,
sourceType,
packages: /* @__PURE__ */ new Map()
};
modules.set(id, created);
return created;
}
function registerModuleInPackage(modules, moduleId, source, sourceType, packageId, fileName) {
const moduleEntry = ensureModule(modules, moduleId, source, sourceType);
const files = moduleEntry.packages.get(packageId) ?? /* @__PURE__ */ new Set();
files.add(fileName);
moduleEntry.packages.set(packageId, files);
}
//#endregion
//#region src/analyze/subpackages/output.ts
function getAssetBuffer(asset) {
if (typeof asset.source === "string") return Buffer.from(asset.source, "utf8");
if (asset.source instanceof Uint8Array) return Buffer.from(asset.source);
}
function getCompressedSizes$1(content) {
if (content === void 0) return {};
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
return {
gzipSize: gzipSync(buffer).byteLength,
brotliSize: brotliCompressSync(buffer).byteLength
};
}
function processChunk(chunk, origin, ctx, classifierContext, packages, modules) {
const classification = classifyPackage(chunk.fileName, origin, classifierContext);
const packageEntry = ensurePackage(packages, classification);
const chunkEntry = {
file: chunk.fileName,
type: "chunk",
from: origin,
size: typeof chunk.code === "string" ? Buffer.byteLength(chunk.code, "utf8") : void 0,
...getCompressedSizes$1(chunk.code),
isEntry: chunk.isEntry,
modules: []
};
const moduleEntries = Object.entries(chunk.modules ?? {});
for (const [rawModuleId, info] of moduleEntries) {
const absoluteId = normalizeModuleId(rawModuleId);
if (!absoluteId) continue;
const { source, sourceType } = resolveModuleSourceType(absoluteId, ctx);
const moduleEntry = {
id: absoluteId,
source,
sourceType,
bytes: info?.renderedLength
};
if (typeof info?.code === "string") moduleEntry.originalBytes = Buffer.byteLength(info.code, "utf8");
chunkEntry.modules.push(moduleEntry);
registerModuleInPackage(modules, absoluteId, source, sourceType, classification.id, chunk.fileName);
}
if (chunkEntry.modules) chunkEntry.modules.sort((a, b) => a.source.localeCompare(b.source));
packageEntry.files.set(chunk.fileName, chunkEntry);
}
function processAsset(asset, origin, ctx, classifierContext, packages, modules) {
const classification = classifyPackage(asset.fileName, origin, classifierContext);
const packageEntry = ensurePackage(packages, classification);
const assetBuffer = getAssetBuffer(asset);
const entry = {
file: asset.fileName,
type: "asset",
from: origin,
size: assetBuffer?.byteLength,
...getCompressedSizes$1(assetBuffer)
};
const assetSource = resolveAssetSource(asset.fileName, ctx);
if (assetSource) {
entry.source = assetSource.source;
registerModuleInPackage(modules, assetSource.absolute, assetSource.source, assetSource.sourceType, classification.id, asset.fileName);
}
packageEntry.files.set(asset.fileName, entry);
}
function processOutput(output, origin, ctx, classifierContext, packages, modules) {
if (!output) return;
for (const item of output.output ?? []) if (item.type === "chunk") processChunk(item, origin, ctx, classifierContext, packages, modules);
else if (item.type === "asset") processAsset(item, origin, ctx, classifierContext, packages, modules);
}
//#endregion
//#region src/analyze/subpackages/summary.ts
function toArray(value) {
return Array.from(value);
}
function summarizePackages(packages) {
const order = {
main: 0,
subPackage: 1,
independent: 2,
virtual: 3
};
const reports = toArray(packages.values()).map((pkg) => {
const files = toArray(pkg.files.values());
files.sort((a, b) => a.file.localeCompare(b.file));
return {
id: pkg.id,
label: pkg.label,
type: pkg.type,
files
};
});
reports.sort((a, b) => {
const delta = order[a.type] - order[b.type];
if (delta !== 0) return delta;
if (a.id === "__main__") return -1;
if (b.id === "__main__") return 1;
return a.id.localeCompare(b.id);
});
return reports;
}
function summarizeModules(modules) {
const usage = toArray(modules.values()).map((module) => {
const packages = toArray(module.packages.entries()).map(([packageId, files]) => {
return {
packageId,
files: toArray(files).sort((a, b) => a.localeCompare(b))
};
}).sort((a, b) => {
if (a.packageId === b.packageId) return 0;
if (a.packageId === "__main__") return -1;
if (b.packageId === "__main__") return 1;
return a.packageId.localeCompare(b.packageId);
});
return {
id: module.id,
source: module.source,
sourceType: module.sourceType,
packages
};
});
usage.sort((a, b) => a.source.localeCompare(b.source));
return usage;
}
function expandVirtualModulePlacements(modules, packages, context) {
for (const moduleEntry of modules.values()) {
const virtualEntries = Array.from(moduleEntry.packages.entries()).filter(([packageId]) => packageId.startsWith("virtual:"));
if (!virtualEntries.length) continue;
const virtualFileBases = /* @__PURE__ */ new Map();
for (const [virtualPackageId, files] of virtualEntries) {
const combination = virtualPackageId.slice(8);
if (!combination) continue;
const segments = combination.split(/[_+]/).map((segment) => segment.trim()).filter(Boolean);
if (!segments.length) continue;
let matchingBases = virtualFileBases.get(virtualPackageId);
if (!matchingBases) {
matchingBases = Array.from(files).map((file) => posix.basename(file));
virtualFileBases.set(virtualPackageId, matchingBases);
}
for (const root of segments) {
if (!context.subPackageRoots.has(root)) continue;
const targetPackage = packages.get(root);
if (!targetPackage) continue;
const moduleFiles = moduleEntry.packages.get(root) ?? /* @__PURE__ */ new Set();
const targetFiles = Array.from(targetPackage.files.values()).filter((fileEntry) => {
if (!matchingBases?.length) return true;
const base = posix.basename(fileEntry.file);
return matchingBases.includes(base);
}).map((fileEntry) => fileEntry.file);
if (targetFiles.length === 0) {
const fallback = targetPackage.files.values().next().value;
if (fallback) moduleFiles.add(fallback.file);
} else for (const fileName of targetFiles) moduleFiles.add(fileName);
if (moduleFiles.size > 0) moduleEntry.packages.set(root, moduleFiles);
}
}
}
}
function summarizeSubPackages(metas) {
const descriptors = metas.map((meta) => {
return {
root: meta.subPackage.root ?? "",
independent: Boolean(meta.subPackage.independent),
name: meta.subPackage.name
};
}).filter((descriptor) => descriptor.root);
descriptors.sort((a, b) => a.root.localeCompare(b.root));
return descriptors;
}
//#endregion
//#region src/analyze/subpackages/index.ts
async function analyzeSubpackages(ctx) {
const { configService, scanService, buildService } = ctx;
if (!configService || !scanService || !buildService) throw new Error("analyzeSubpackages 需要先初始化 configService、scanService 和 buildService。");
await scanService.loadAppEntry();
const subPackageMetas = scanService.loadSubPackages();
const subPackageRoots = /* @__PURE__ */ new Set();
const independentRoots = /* @__PURE__ */ new Set();
for (const meta of subPackageMetas) {
const root = meta.subPackage.root;
if (root) {
subPackageRoots.add(root);
if (meta.subPackage.independent) independentRoots.add(root);
}
}
const classifierContext = {
subPackageRoots,
independentRoots
};
const analysisConfig = configService.merge(void 0, createSharedBuildConfig(configService, scanService), { build: {
write: false,
watch: null
} });
const mainResult = await build(analysisConfig);
const mainOutputs = Array.isArray(mainResult) ? mainResult : [mainResult];
const packages = /* @__PURE__ */ new Map();
const modules = /* @__PURE__ */ new Map();
const componentJsonConfigs = [];
for (const output of mainOutputs) {
processOutput(output, "main", ctx, classifierContext, packages, modules);
componentJsonConfigs.push(...collectAnalyzeComponentJsonConfigs(output));
}
for (const root of independentRoots) {
const output = buildService.getIndependentOutput(root);
processOutput(output, "independent", ctx, classifierContext, packages, modules);
componentJsonConfigs.push(...collectAnalyzeComponentJsonConfigs(output));
}
expandVirtualModulePlacements(modules, packages, classifierContext);
const subPackages = summarizeSubPackages(subPackageMetas);
return {
metadata: createAnalyzeMetadata(ctx.configService),
packages: summarizePackages(packages),
modules: summarizeModules(modules),
subPackages,
components: analyzeComponentUsage({
jsonConfigs: componentJsonConfigs,
subPackages
}),
glassEasel: createGlassEaselAnalyzeResult(ctx)
};
}
//#endregion
//#region src/analyze/subpackages/history.ts
const jsonExtension = ".json";
function createSnapshotFileName(date = /* @__PURE__ */ new Date()) {
return `${date.toISOString().replace(/[:.]/g, "-")}${jsonExtension}`;
}
function isAnalyzeResult(value) {
return Boolean(value && typeof value === "object" && Array.isArray(value.packages) && Array.isArray(value.modules) && Array.isArray(value.subPackages));
}
async function listSnapshotFiles(dir) {
try {
return (await fs$2.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(jsonExtension)).map((entry) => path.join(dir, entry.name)).sort((a, b) => b.localeCompare(a));
} catch {
return [];
}
}
async function trimSnapshotFiles(dir, limit) {
const staleFiles = (await listSnapshotFiles(dir)).slice(limit);
await Promise.all(staleFiles.map(async (file) => {
try {
await fs$2.unlink(file);
} catch {}
}));
}
async function readLatestAnalyzeHistorySnapshot(configService) {
const history = resolveAnalyzeHistoryMetadata(configService);
if (!history.enabled) return null;
const files = await listSnapshotFiles(history.dir);
for (const file of files) try {
const parsed = JSON.parse(await fs$2.readFile(file, "utf8"));
if (isAnalyzeResult(parsed)) return parsed;
} catch {}
return null;
}
async function writeAnalyzeHistorySnapshot(result, configService, now = /* @__PURE__ */ new Date()) {
const history = resolveAnalyzeHistoryMetadata(configService);
if (!history.enabled) return;
await fs$2.mkdir(history.dir, { recursive: true });
const snapshotPath = path.join(history.dir, createSnapshotFileName(now));
if (result.metadata) result.metadata = {
...result.metadata,
history: {
...history,
dir: configService.relativeCwd(history.dir),
latestSnapshot: configService.relativeCwd(snapshotPath)
}
};
await fs$2.writeFile(snapshotPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
await trimSnapshotFiles(history.dir, history.limit);
return snapshotPath;
}
//#endregion
//#region src/analyze/subpackages/report.ts
function formatAnalyzeBytes(bytes) {
if (!bytes || Number.isNaN(bytes)) return "0 B";
const units = [
"B",
"KB",
"MB",
"GB"
];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(value >= 100 || unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
}
function formatDelta(bytes) {
if (typeof bytes !== "number" || Number.isNaN(bytes) || bytes === 0) return "无变化";
return `${bytes > 0 ? "+" : "-"}${formatAnalyzeBytes(Math.abs(bytes))}`;
}
function getFileSize(file) {
return file.size ?? 0;
}
function getCompressedSize(file) {
return file.brotliSize ?? file.gzipSize ?? 0;
}
function getBudgetLimit(type, budgets) {
if (!budgets) return;
if (type === "main") return budgets.mainBytes;
if (type === "subPackage") return budgets.subPackageBytes;
if (type === "independent") return budgets.independentBytes;
}
function createPackageSizeMap(result) {
return new Map((result?.packages ?? []).map((pkg) => {
const totalBytes = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
return [pkg.id, totalBytes];
}));
}
function createFileKey(packageId, fileName) {
return `${packageId}\u0000${fileName}`;
}
function createFileSizeMap(result) {
const map = /* @__PURE__ */ new Map();
for (const pkg of result?.packages ?? []) for (const file of pkg.files) map.set(createFileKey(pkg.id, file.file), getFileSize(file));
return map;
}
function createPackageTypeMap(result) {
return new Map(result.packages.map((pkg) => [pkg.id, pkg.type]));
}
function createModuleByteMap(result) {
const map = /* @__PURE__ */ new Map();
for (const pkg of result.packages) for (const file of pkg.files) for (const mod of file.modules ?? []) {
const bytes = mod.bytes ?? mod.originalBytes ?? 0;
map.set(mod.id, Math.max(map.get(mod.id) ?? 0, bytes));
}
return map;
}
function classifyIncrementCategory(source, sourceType) {
if (source.includes("wevu") || source.includes("@weapp-vite/dashboard")) return "WeVu / runtime";
if (sourceType === "node_modules" || source.includes("node_modules")) return "第三方依赖";
if (sourceType === "workspace") return "工作区包";
if (sourceType === "plugin") return "插件生成";
if (source.endsWith(".wxss") || source.endsWith(".css") || source.endsWith(".scss")) return "样式资源";
if (source.endsWith(".wxml") || source.endsWith(".json")) return "页面结构";
return "业务源码";
}
function createIncrementAdvice(item) {
if (item.category === "第三方依赖") return "检查依赖是否只在必要分包引用,必要时收敛到公共入口或替换轻量实现。";
if (item.category === "WeVu / runtime") return "确认运行时能力是否被新页面引入,优先排查组件和 API 引用边界。";
if (item.category === "样式资源") return "检查样式重复、原子类生成范围和组件样式裁剪。";
if (item.type === "new-file" || item.type === "new-module") return "确认是否为本次需求必要新增,评估分包归属和懒加载边界。";
return "对比本次变更,优先查看新增引用、共享模块和大对象常量。";
}
function createModuleSizeMap(result) {
const map = /* @__PURE__ */ new Map();
for (const pkg of result?.packages ?? []) for (const file of pkg.files) for (const module of file.modules ?? []) {
const bytes = module.bytes ?? module.originalBytes ?? 0;
const existing = map.get(module.id);
if (existing && existing.bytes >= bytes) continue;
map.set(module.id, {
source: module.source,
sourceType: module.sourceType,
bytes,
packageLabel: pkg.label,
file: file.file
});
}
return map;
}
function createDuplicateAdvice(sourceType, packageIds, packageTypeMap, estimatedSavingBytes) {
if (packageIds.some((packageId) => packageTypeMap.get(packageId) === "independent")) return estimatedSavingBytes > 0 ? "含独立分包,先确认隔离要求,再评估是否抽公共入口。" : "含独立分包,重复可能来自隔离边界。";
if (sourceType === "node_modules") return "依赖被多个包带入,检查引用边界或考虑主包公共入口。";
if (sourceType === "src" || sourceType === "workspace") return "共享源码跨包重复,优先抽公共模块或调整分包归属。";
if (sourceType === "plugin") return "插件生成内容跨包重复,检查插件产物输出策略。";
return "检查该模块是否需要在多个包内重复存在。";
}
function formatBudgetStatus(item) {
if (item.status === "ok") return "正常";
return `${item.status === "exceeded" ? "超预算" : "接近预算"} ${(item.ratio * 100).toFixed(1)}%`;
}
function createActionItems(options) {
const actions = [];
const exceededItems = options.budgetItems.filter((item) => item.status === "exceeded");
const warningItems = options.budgetItems.filter((item) => item.status === "warning");
const topDuplicate = options.duplicateInsights.find((item) => item.estimatedSavingBytes > 0);
if (exceededItems.length > 0) actions.push(`处理 ${exceededItems[0].label} 预算超限:当前 ${formatAnalyzeBytes(exceededItems[0].currentBytes)},限制 ${formatAnalyzeBytes(exceededItems[0].limitBytes)}。`);
else if (warningItems.length > 0) actions.push(`关注 ${warningItems[0].label} 预算接近阈值:当前 ${(warningItems[0].ratio * 100).toFixed(1)}%。`);
if (topDuplicate) actions.push(`优先处理重复模块 ${topDuplicate.source},估算可节省 ${formatAnalyzeBytes(topDuplicate.estimatedSavingBytes)}。`);
if (actions.length === 0) actions.push("当前没有预算超限或高收益重复模块,保持观察即可。");
return actions;
}
function createAnalyzeBudgetCheck(result) {
const budgets = result.metadata?.budgets;
if (!budgets) return [];
const items = [];
const totalBytes = result.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
const warningRatio = budgets.warningRatio;
const createItem = (options) => {
const ratio = options.limitBytes > 0 ? options.currentBytes / options.limitBytes : 0;
const status = ratio >= 1 ? "exceeded" : ratio >= warningRatio ? "warning" : "ok";
return {
...options,
ratio,
status
};
};
items.push(createItem({
id: "__total__",
label: "总包",
scope: "total",
currentBytes: totalBytes,
limitBytes: budgets.totalBytes
}));
for (const pkg of result.packages) {
const limitBytes = getBudgetLimit(pkg.type, budgets);
if (!limitBytes) continue;
items.push(createItem({
id: pkg.id,
label: pkg.label,
scope: pkg.type,
currentBytes: pkg.files.reduce((sum, file) => sum + getFileSize(file), 0),
limitBytes
}));
}
return items.sort((a, b) => b.ratio - a.ratio || a.label.localeCompare(b.label));
}
function createDuplicateModuleInsights(result) {
const moduleByteMap = createModuleByteMap(result);
const packageTypeMap = createPackageTypeMap(result);
return result.modules.filter((module) => module.packages.length > 1).map((module) => {
const bytes = moduleByteMap.get(module.id) ?? 0;
const packageIds = module.packages.map((pkg) => pkg.packageId);
const estimatedSavingBytes = bytes * Math.max(module.packages.length - 1, 0);
return {
id: module.id,
source: module.source,
sourceType: module.sourceType,
packageCount: module.packages.length,
bytes,
estimatedSavingBytes,
packages: packageIds,
advice: createDuplicateAdvice(module.sourceType, packageIds, packageTypeMap, estimatedSavingBytes)
};
}).sort((a, b) => b.estimatedSavingBytes - a.estimatedSavingBytes || b.packageCount - a.packageCount || a.source.localeCompare(b.source));
}
function createAnalyzeIncrementAttribution(result, previousResult) {
if (!previousResult) return [];
const previousFiles = createFileSizeMap(previousResult);
const previousModules = createModuleSizeMap(previousResult);
const currentModules = createModuleSizeMap(result);
const items = [];
for (const pkg of result.packages) for (const file of pkg.files) {
const currentBytes = getFileSize(file);
const previousBytes = previousFiles.get(createFileKey(pkg.id, file.file)) ?? 0;
const deltaBytes = currentBytes - previousBytes;
if (deltaBytes <= 0) continue;
const type = previousBytes > 0 ? "increased-file" : "new-file";
const item = {
key: `file:${pkg.id}:${file.file}`,
type,
label: file.file,
category: classifyIncrementCategory(file.source ?? file.file),
packageLabel: pkg.label,
file: file.file,
currentBytes,
previousBytes,
deltaBytes,
advice: ""
};
items.push({
...item,
advice: createIncrementAdvice(item)
});
}
for (const [id, module] of currentModules) {
const previousBytes = previousModules.get(id)?.bytes ?? 0;
const deltaBytes = module.bytes - previousBytes;
if (deltaBytes <= 0) continue;
const type = previousBytes > 0 ? "increased-module" : "new-module";
const item = {
key: `module:${id}`,
type,
label: module.source,
category: classifyIncrementCategory(module.source, module.sourceType),
packageLabel: module.packageLabel,
file: module.file,
currentBytes: module.bytes,
previousBytes,
deltaBytes,
advice: ""
};
items.push({
...item,
advice: createIncrementAdvice(item)
});
}
return items.sort((a, b) => b.deltaBytes - a.deltaBytes || a.category.localeCompare(b.category) || a.label.localeCompare(b.label));
}
function createAnalyzeIncrementCategorySummary(items) {
const map = /* @__PURE__ */ new Map();
for (const item of items) {
const entry = map.get(item.category) ?? {
category: item.category,
count: 0,
deltaBytes: 0
};
entry.count += 1;
entry.deltaBytes += item.deltaBytes;
map.set(item.category, entry);
}
return [...map.values()].sort((a, b) => b.deltaBytes - a.deltaBytes || b.count - a.count || a.category.localeCompare(b.category));
}
function createAnalyzePrMarkdownReport(result, previousResult) {
const files = result.packages.flatMap((pkg) => pkg.files.map((file) => ({
pkg,
file
})));
const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
const duplicateInsights = createDuplicateModuleInsights(result);
const budgetItems = createAnalyzeBudgetCheck(result);
const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
const actionItems = createActionItems({
budgetItems,
duplicateInsights
}).slice(0, 3);
const budgetRows = budgetIssues.slice(0, 5).map((item) => `| ${item.label} | ${formatAnalyzeBytes(item.currentBytes)} | ${formatAnalyzeBytes(item.limitBytes)} | ${formatBudgetStatus(item)} |`).join("\n");
const incrementRows = incrementItems.slice(0, 8).map((item) => `| ${item.label} | ${item.category} | ${item.packageLabel} | ${formatAnalyzeBytes(item.deltaBytes)} | ${item.advice} |`).join("\n");
const sourceRows = incrementSummary.slice(0, 6).map((item) => `| ${item.category} | ${item.count} | ${formatAnalyzeBytes(item.deltaBytes)} |`).join("\n");
const duplicateRows = duplicateInsights.slice(0, 5).map((module) => `| ${module.source} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
return [
"## weapp-vite analyze PR 摘要",
"",
`- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)})`,
`- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
`- 预算告警:${budgetIssues.length}`,
`- 增量归因:${incrementItems.length > 0 ? `${incrementItems.length} 项正向增长` : "无正向增长"}`,
`- 跨包复用:${duplicateInsights.length}`,
"",
"### 建议动作",
"",
...actionItems.map((item) => `- ${item}`),
"",
"### 预算状态",
"",
"| 对象 | 当前体积 | 预算 | 状态 |",
"| --- | ---: | ---: | --- |",
budgetRows || "| - | 0 B | 0 B | 正常 |",
"",
"### 增量来源",
"",
"| 来源 | 项数 | 增量 |",
"| --- | ---: | ---: |",
sourceRows || "| - | 0 | 0 B |",
"",
"### Top 增量",
"",
"| 文件/模块 | 来源 | 包 | 增量 | 建议 |",
"| --- | --- | --- | ---: | --- |",
incrementRows || "| - | - | - | 0 B | - |",
"",
"### 重复模块",
"",
"| 模块 | 包数量 | 估算可节省 | 建议 |",
"| --- | ---: | ---: | --- |",
duplicateRows || "| - | 0 | 0 B | - |",
""
].join("\n");
}
function createAnalyzeMarkdownReport(result, previousResult) {
const files = result.packages.flatMap((pkg) => pkg.files.map((file) => ({
pkg,
file
})));
const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
const duplicateInsights = createDuplicateModuleInsights(result);
const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
const budgetItems = createAnalyzeBudgetCheck(result);
const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
const previousPackageSizes = createPackageSizeMap(previousResult);
const budgets = result.metadata?.budgets;
const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
const actionItems = createActionItems({
budgetItems,
duplicateInsights
});
const packageRows = result.packages.map((pkg) => {
const size = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
const compressed = pkg.files.reduce((sum, file) => sum + getCompressedSize(file), 0);
const previousSize = previousPackageSizes.get(pkg.id);
const budgetStatus = budgetItems.find((item) => item.id === pkg.id);
return `| ${pkg.label} | ${pkg.type} | ${formatAnalyzeBytes(size)} | ${formatAnalyzeBytes(compressed)} | ${formatDelta(typeof previousSize === "number" ? size - previousSize : void 0)} | ${budgetStatus ? formatBudgetStatus(budgetStatus) : "正常"} |`;
}).join("\n");
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");
const duplicateRows = duplicateInsights.slice(0, 10).map((module) => `| ${module.source} | ${module.sourceType} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
const budgetRows = budgetIssues.map((item) => `| ${item.label} | ${item.scope} | ${formatAnalyzeBytes(item.currentBytes)} | ${formatAnalyzeBytes(item.limitBytes)} | ${formatBudgetStatus(item)} |`).join("\n");
const incrementRows = incrementItems.slice(0, 10).map((item) => `| ${item.label} | ${item.category} | ${item.packageLabel} | ${formatAnalyzeBytes(item.deltaBytes)} | ${item.advice} |`).join("\n");
const incrementSummaryRows = incrementSummary.slice(0, 8).map((item) => `| ${item.category} | ${item.count} | ${formatAnalyzeBytes(item.deltaBytes)} |`).join("\n");
return [
"# weapp-vite analyze 报告",
"",
`生成时间:${result.metadata?.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()}`,
"",
"## 本次变化摘要",
"",
`- 总产物体积:${formatAnalyzeBytes(totalBytes)}`,
`- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
`- 较上次:${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)}`,
`- 包体数量:${result.packages.length}`,
`- 源码模块:${result.modules.length}`,
`- 跨包复用:${duplicateInsights.length}`,
`- 预算来源:${budgets?.source === "config" ? "配置" : "默认"}`,
"",
"## 预算告警",
"",
"| 对象 | 范围 | 当前体积 | 预算 | 状态 |",
"| --- | --- | ---: | ---: | --- |",
budgetRows || "| - | - | 0 B | 0 B | 正常 |",
"",
"## 建议动作",
"",
...actionItems.map((item) => `- ${item}`),
"",
"## 增量归因",
"",
"| 来源 | 项数 | 增量 |",
"| --- | ---: | ---: |",
incrementSummaryRows || "| - | 0 | 0 B |",
"",
"| 文件/模块 | 来源 | 包 | 增量 | 建议 |",
"| --- | --- | --- | ---: | --- |",
incrementRows || "| - | - | - | 0 B | - |",
"",
"## 包体预算",
"",
"| 包 | 类型 | 体积 | 压缩后 | 较上次 | 预算 |",
"| --- | --- | ---: | ---: | ---: | --- |",
packageRows || "| - | - | 0 B | 0 B | 无变化 | 正常 |",
"",
"## Top 文件",
"",
"| 文件 | 包 | 类型 | 体积 | 压缩后 |",
"| --- | --- | --- | ---: | ---: |",
topFileRows || "| - | - | - | 0 B | 0 B |",
"",
"## 重复模块",
"",
"| 模块 | 来源 | 包数量 | 估算可节省 | 建议 |",
"| --- | --- | ---: | ---: | --- |",
duplicateRows || "| - | - | 0 | 0 B | - |",
""
].join("\n");
}
//#endregion
//#region src/cli/analyze/dashboard.ts
const ANALYZE_GLOBAL_KEY = "__WEAPP_VITE_ANALYZE_RESULT__";
const PREVIOUS_ANALYZE_GLOBAL_KEY = "__WEAPP_VITE_PREVIOUS_ANALYZE_RESULT__";
const DASHBOARD_EVENTS_GLOBAL_KEY = "__WEAPP_VITE_DASHBOARD_EVENTS__";
const ANALYZE_DASHBOARD_PACKAGE_NAME = "@weapp-vite/dashboard";
const ANALYZE_SSE_PATH = "/__weapp_vite_analyze";
const FILE_CONTENT_PATH = "/__weapp_vite_file_content";
const DASHBOARD_EVENT_NAME = "weapp-dashboard:event";
const MAX_FILE_CONTENT_BYTES = 2097152;
const require = createRequire(import.meta.url);
function createInstallCommand(agent) {
const resolved = resolveCommand(agent ?? "npm", "install", [ANALYZE_DASHBOARD_PACKAGE_NAME]);
if (!resolved) return `npm install ${ANALYZE_DASHBOARD_PACKAGE_NAME}`;
return `${resolved.command} ${resolved.args.join(" ")}`;
}
function formatEventTimestamp(date = /* @__PURE__ */ new Date()) {
return date.toLocaleTimeString("zh-CN", { hour12: false });
}
function createDashboardRuntimeEvent(input) {
return {
id: `dashboard:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
kind: input.kind,
level: input.level,
title: input.title,
detail: input.detail,
timestamp: formatEventTimestamp(),
source: input.source ?? "weapp-vite",
durationMs: input.durationMs,
tags: input.tags,
profile: input.profile
};
}
function readDashboardManifest(packageJsonPath) {
try {
return parseCommentJson(fs.readFileSync(packageJsonPath, "utf8"));
} catch {
return;
}
}
function resolveDashboardDistRoot(packageRoot, manifest) {
const distDir = manifest?.weappViteDashboard?.distDir ?? "dist";
const distRoot = path.resolve(packageRoot, distDir);
if (!fs.existsSync(distRoot)) return;
return { root: distRoot };
}
function resolveDashboardDevRoot(packageRoot, manifest) {
const devRoot = manifest?.weappViteDashboard?.devRoot;
const devConfigFile = manifest?.weappViteDashboard?.devConfigFile;
if (!devRoot || !devConfigFile) return;
const root = path.resolve(packageRoot, devRoot);
const configFile = path.resolve(root, devConfigFile);
if (!fs.existsSync(root) || !fs.existsSync(configFile)) return;
return {
root,
configFile
};
}
function resolveDashboardRoot(options) {
const resolvePaths = options?.cwd && options.cwd !== process.cwd() ? [options.cwd, process.cwd()] : options?.cwd ? [options.cwd] : void 0;
let dashboardPackageRoot;
let dashboardManifest;
try {
const dashboardPackageJsonPath = require.resolve(`${ANALYZE_DASHBOARD_PACKAGE_NAME}/package.json`, { paths: resolvePaths });
dashboardPackageRoot = path.dirname(dashboardPackageJsonPath);
dashboardManifest = readDashboardManifest(dashboardPackageJsonPath);
} catch {
dashboardPackageRoot = void 0;
dashboardManifest = void 0;
}
if (dashboardPackageRoot) {
const devResolved = resolveDashboardDevRoot(dashboardPackageRoot, dashboardManifest);
if (devResolved) return devResolved;
const distResolved = resolveDashboardDistRoot(dashboardPackageRoot, dashboardManifest);
if (distResolved) return distResolved;
}
logger_default.warn(`[weapp-vite ui] 未安装可选仪表盘包 ${colors.bold(colors.green(ANALYZE_DASHBOARD_PACKAGE_NAME))},已自动降级关闭 dashboard 能力。`);
logger_default.info(`如需启用,请执行 ${colors.bold(colors.green(createInstallCommand(options?.packageManagerAgent)))}`);
}
function normalizeDashboardRelativePath(value) {
return value.replaceAll("\\", "/");
}
function stripDashboardFileQuery(value) {
const queryIndex = value.indexOf("?");
return queryIndex === -1 ? value : value.slice(0, queryIndex);
}
function addDashboardAllowedPath(paths, value) {
if (!value || value.includes("\0")) return;
const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
paths.add(normalizedPath);
}
function createDashboardContentAllowlist(result) {
const artifactPaths = /* @__PURE__ */ new Set();
const sourcePaths = /* @__PURE__ */ new Set();
for (const packageReport of result.packages) for (const file of packageReport.files) {
addDashboardAllowedPath(artifactPaths, file.file);
addDashboardAllowedPath(sourcePaths, file.source);
for (const module of file.modules ?? []) addDashboardAllowedPath(sourcePaths, module.source);
}
return {
artifactPaths,
sourcePaths
};
}
function resolveDashboardContentPath(root, requestPath, options) {
if (!root || !requestPath || requestPath.includes("\0")) return;
const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
if (path.isAbsolute(normalizedRequestPath)) return;
if (!options.allowedPaths.has(normalizedRequestPath)) return;
const resolvedRoot = path.resolve(root);
const absolutePath = path.resolve(resolvedRoot, normalizedRequestPath);
const relativePath = path.relative(resolvedRoot, absolutePath);
if (!relativePath) return;
if (!options.allowParent && (relativePath.startsWith("..") || path.isAbsolute(relativePath))) return;
return {
absolutePath,
relativePath: options.allowParent ? normalizedRequestPath : normalizeDashboardRelativePath(relativePath)
};
}
function resolveDashboardFileLanguage(filePath) {
const extension = path.extname(filePath).toLowerCase();
if (extension === ".js" || extension === ".mjs" || extension === ".cjs" || extension === ".wxs" || extension === ".sjs") return "javascript";
if (extension === ".ts" || extension === ".mts" || extension === ".cts") return "typescript";
if (extension === ".json" || extension === ".map") return "json";
if (extension === ".css" || extension === ".wxss" || extension === ".scss" || extension === ".sass" || extension === ".less") return "css";
if (extension === ".vue" || extension === ".wxml" || extension === ".html") return "html";
return "plaintext";
}
function sendDashboardJson(res, statusCode, payload) {
res.statusCode = statusCode;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(payload));
}
async function sendDashboardFileContent(res, roots, allowlist, kind, requestPath) {
const resolved = resolveDashboardContentPath(kind === "artifact" ? roots.artifactRoot : kind === "source" ? roots.sourceRoot : void 0, requestPath, {
allowParent: kind === "source",
allowedPaths: kind === "artifact" ? allowlist.artifactPaths : allowlist.sourcePaths
});
if (!resolved || kind !== "source" && kind !== "artifact") {
sendDashboardJson(res, 400, {
error: "invalid_request",
message: "必须传入合法的 kind 和相对路径。"
});
return;
}
try {
const stat = await fs.promises.stat(resolved.absolutePath);
if (!stat.isFile()) {
sendDashboardJson(res, 400, {
error: "not_file",
message: "目标路径不是文件。"
});
return;
}
if (stat.size > MAX_FILE_CONTENT_BYTES) {
sendDashboardJson(res, 413, {
error: "file_too_large",
message: `文件超过 ${MAX_FILE_CONTENT_BYTES} 字节,已拒绝读取。`
});
return;
}
sendDashboardJson(res, 200, {
kind,
path: resolved.relativePath,
language: resolveDashboardFileLanguage(resolved.relativePath),
size: stat.size,
content: await fs.promises.readFile(resolved.absolutePath, "utf8")
});
} catch (error) {
const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
sendDashboardJson(res, code === "ENOENT" ? 404 : 500, {
error: code === "ENOENT" ? "not_found" : "read_failed",
message: code === "ENOENT" ? "文件不存在。" : "读取文件失败。"
});
}
}
function createAnalyzeHtmlPlugin(state, runtimeEvents, contentRoots, contentAllowlist, onServerInstance, onBroadcastReady) {
const sseClients = /* @__PURE__ */ new Set();
const hotBridgeScript = `
const applyAnalyzePayload = (payload) => {
const current = payload?.current ?? payload
const previous = payload?.previous ?? window.${PREVIOUS_ANALYZE_GLOBAL_KEY} ?? null
window.${ANALYZE_GLOBAL_KEY} = current
window.${PREVIOUS_ANALYZE_GLOBAL_KEY} = previous
window.dispatchEvent(new CustomEvent('weapp-analyze:update', { detail: { current, previous } }))
}
const applyDashboardEvents = (payload) => {
const events = Array.isArray(payload) ? payload : [payload]
const nextEvents = events.filter(Boolean)
if (nextEvents.length === 0) {
return
}
window.${DASHBOARD_EVENTS_GLOBAL_KEY} = [
...(window.${DASHBOARD_EVENTS_GLOBAL_KEY} ?? []),
...nextEvents,
]
window.dispatchEvent(new CustomEvent('${DASHBOARD_EVENT_NAME}', { detail: nextEvents }))
}
const source = new EventSource('${ANALYZE_SSE_PATH}')
source.onmessage = (event) => {
try {
applyAnalyzePayload(JSON.parse(event.data))
}
catch {}
}
if (import.meta.hot) {
import.meta.hot.on('weapp-analyze:update', (payload) => {
applyAnalyzePayload(payload)
})
import.meta.hot.on('${DASHBOARD_EVENT_NAME}', (payload) => {
applyDashboardEvents(payload)
})
}
`.trim();
const broadcast = (payload) => {
const serialized = `data: ${JSON.stringify(payload)}\n\n`;
for (const client of sseClients) client.write(serialized);
};
onBroadcastReady(broadcast);
return {
name: "weapp-vite-analyze-html",
transformIndexHtml(html) {
return {
html,
tags: [
{
tag: "script",
children: `window.${ANALYZE_GLOBAL_KEY} = ${JSON.stringify(state.current)}`,
injectTo: "head-prepend"
},
{
tag: "script",
children: `window.${PREVIOUS_ANALYZE_GLOBAL_KEY} = ${JSON.stringify(state.previous)}`,
injectTo: "head-prepend"
},
{
tag: "script",
children: `window.${DASHBOARD_EVENTS_GLOBAL_KEY} = ${JSON.stringify(runtimeEvents.current)}`,
injectTo: "head-prepend"
},
{
tag: "script",
attrs: {
type: "module",
src: "/@vite/client"
},
injectTo: "head"
},
{
tag: "script",
attrs: { type: "module" },
children: hotBridgeScript,
injectTo: "body"
}
]
};
},
configureServer(server) {
onServerInstance(server);
server.middlewares.use((req, res, next) => {
const url = new URL(req.url ?? "/", "http://127.0.0.1");
if (url.pathname === FILE_CONTENT_PATH) {
sendDashboardFileContent(res, contentRoots, contentAllowlist.current, url.searchParams.get("kind"), url.searchParams.get("path"));
return;
}
if (url.pathname !== ANALYZE_SSE_PATH) {
next();
return;
}
res.statusCode = 200;
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.write(`data: ${JSON.stringify(state)}\n\n`);
sseClients.add(res);
req.on("close", () => {
sseClients.delete(res);
});
});
}
};
}
async function waitForServerExit(server) {
let resolved = false;
const cleanup = async () => {
if (resolved) return;
resolved = true;
try {
await server.close();
} catch (error) {
logger_default.error(error);
}
};
const signals = ["SIGINT", "SIGTERM"];
await new Promise((resolvePromise) => {
const resolveOnce = async () => {
await cleanup();
signals.forEach((signal) => {
process.removeListener(signal, resolveOnce);
});
resolvePromise();
};
signals.forEach((signal) => {
process.once(signal, resolveOnce);
});
server.httpServer?.once("close", resolveOnce);
});
}
async function startAnalyzeDashboard(result, options) {
const resolved = resolveDashboardRoot(options);
if (!resolved) return;
const { root, configFile } = resolved;
const state = {
current: result,
previous: options?.previousResult ?? null
};
const contentAllowlist = { current: createDashboardContentAllowlist(result) };
const runtimeEvents = { current: [createDashboardRuntimeEvent({
kind: "command",
level: "success",
title: options?.watch ? "dashboard watch session started" : "dashboard static session started",
detail: options?.watch ? "weapp-vite UI 已进入实时分析模式,后续 analyze 结果会继续推送到 dashboard。" : "weapp-vite UI 已进入静态分析模式,当前页面展示的是一次性分析结果。",
tags: options?.watch ? ["watch", "analyze"] : ["static", "analyze"]
}), ...(options?.initialEvents ?? []).map((event) => createDashboardRuntimeEvent(event))] };
let serverRef;
let broadcastAnalyzeResult;
const plugins = [createAnalyzeHtmlPlugin(state, runtimeEvents, {
artifactRoot: options?.artifactRoot ?? (options?.cwd ? path.resolve(options.cwd, "dist") : void 0),
sourceRoot: options?.cwd
}, contentAllowlist, (server) => {
serverRef = server;
}, (broadcast) => {
broadcastAnalyzeResult = broadcast;
})];
const serverOptions = {
root,
configFile: configFile ?? false,
clearScreen: false,
appType: "spa",
publicDir: false,
plugins,
server: {
host: "127.0.0.1",
port: 0,
watch: { ignored: ["**/*"] }
},
logLevel: "error"
};
const server = await createServer(serverOptions);
const requestedPort = typeof serverOptions.server?.port === "number" ? serverOptions.server.port : void 0;
await server.listen(requestedPort);
serverRef ??= server;
server.printUrls();
const urls = (() => {
const resolved = server.resolvedUrls;
if (!resolved) return [];
return [...resolved.local ?? [], ...resolved.network ?? []];
})();
const waitPromise = waitForServerExit(server);
if (serverRef?.ws) {
serverRef.ws.send({
type: "custom",
event: "weapp-analyze:update",
data: state
});
serverRef.ws.send({
type: "custom",
event: DASHBOARD_EVENT_NAME,
data: runtimeEvents.current
});
}
broadcastAnalyzeResult?.(state);
const emitRuntimeEvents = (events) => {
if (events.length === 0) return;
const nextEvents = events.map((event) => createDashboardRuntimeEvent(event));
runtimeEvents.current = [...nextEvents, ...runtimeEvents.current].slice(0, 24);
if (serverRef) serverRef.ws.send({
type: "custom",
event: DASHBOARD_EVENT_NAME,
data: nextEvents
});
};
const handle = {
async update(nextResult, previousResult) {
state.previous = previousResult ?? state.current;
state.current = nextResult;
contentAllowlist.current = createDashboardContentAllowlist(nextResult);
emitRuntimeEvents([{
kind: "build",
level: "info",
title: "analyze payload refreshed",
detail: `已推送新的 analyze 结果,当前包含 ${nextResult.packages.length} 个包与 ${nextResult.modules.length} 个模块。`,
tags: ["analyze", "refresh"]
}]);
if (serverRef) serverRef.ws.send({
type: "custom",
event: "weapp-analyze:update",
data: state
});
broadcastAnalyzeResult?.(state);
},
emitRuntimeEvents,
waitForExit: () => waitPromise,
close: async () => {
await server.close();
},
urls
};
if (options?.watch) {
if (!options.silentStartupLog) {
logger_default.info("weapp-vite UI 已启动(分析视图,实时模式),按 Ctrl+C 退出。");
for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
}
return handle;
}
if (!options?.silentStartupLog) {
logger_default.info("weapp-vite UI 已启动(分析视图,静态模式),按 Ctrl+C 退出。");
for (const url of handle.urls) logger_default.info(` ➜ ${colors.bold(colors.cyan(url))}`);
}
await waitPromise;
}
//#endregion
//#region src/cli/processCleanup.ts
function isSassEmbeddedChild(handle) {
if (!handle || typeof handle !== "object") return false;
const child = handle;
const spawnfile = typeof child.spawnfile === "string" ? child.spawnfile : "";
const spawnargs = Array.isArray(child.spawnargs) ? child.spawnargs : [];
return Boolean("kill" in handle && "spawnfile" in handle && (spawnfile.includes("sass-embedded") || spawnfile.includes("dart-sass") && spawnargs.includes("--embedded")));
}
function terminateStaleSassEmbeddedProcess() {
const getHandles = process._getActiveHandles;
const handles = typeof getHandles === "function" ? getHandles() : void 0;
if (!Array.isArray(handles)) return;
for (const handle of handles) if (isSassEmbeddedChild(handle)) try {
handle.kill();
} catch {}
}
//#endregion
//#region src/cli/commands/analyze.ts
function normalizeDisplayPath(value) {
return value || ".";
}
function getDefaultWebAnalyzeScopes() {
return {
supported: [
"weapp.web 配置解析(enable/root/srcDir/outDir)",
"runtime.executionMode 静态解析(compat/safe/strict)",
"JSON 报告输出(--json/--output)"
],
unsupported: [
"分包产物体积分析(仅小程序)",
"源码模块包体映射(仅小程序)",
"分析仪表盘(dashboard)"
]
};
}
function createWebAnalyzeResult(configService, options) {
const webConfig = configService.weappWebConfig;
const executionMode = webConfig?.pluginOptions.runtime?.executionMode ?? "compat";
const scope = getDefaultWebAnalyzeScopes();
const limitations = ["当前仅提供静态配置分析,不执行 Web 产物扫描。"];
if (!webConfig?.enabled) limitations.push("未检测到启用的 weapp.web 配置。");
return {
runtime: "web",
platform: options.platform,
mode: configService.mode,
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
experimental: true,
configFile: configService.configFilePath ? normalizeDisplayPath(configService.relativeCwd(configService.configFilePath)) : void 0,
web: {
enabled: Boolean(webConfig?.enabled),
root: webConfig?.root ? normalizeDisplayPath(configService.relativeCwd(webConfig.root)) : void 0,
srcDir: webConfig?.srcDir,
outDir: webConfig?.outDir ? normalizeDisplayPath(configService.relativeCwd(webConfig.outDir)) : void 0,
executionMode
},
supportedScopes: scope.supported,
unsupportedScopes: scope.unsupported,
limitations
};
}
function printAnalysisSummary(result) {
const packageLabelMap = /* @__PURE__ */ new Map();
const packageModuleSet = /* @__PURE__ */ new Map();
for (const pkg of result.packages) packageLabelMap.set(pkg.id, pkg.label);
for (const module of result.modules) for (const pkgRef of module.packages) {
const set = packageModuleSet.get(pkgRef.packageId) ?? /* @__PURE__ */ new Set();
set.add(module.id);
packageModuleSet.set(pkgRef.packageId, set);
}
logger_default.success("分包分析完成");
for (const pkg of result.packages) {
const chunkCount = pkg.files.filter((file) => file.type === "chunk").length;
const assetCount = pkg.files.length - chunkCount;
const moduleCount = packageModuleSet.get(pkg.id)?.size ?? 0;
logger_default.info(`- ${pkg.label}:${chunkCount} 个模块产物,${assetCount} 个资源,覆盖 ${moduleCount} 个源码模块`);
}
if (result.subPackages.length > 0) {
logger_default.info("分包配置:");
for (const descriptor of result.subPackages) {
const segments = [descriptor.root];
if (descriptor.name) segments.push(`别名:${descriptor.name}`);
if (descriptor.independent) segments.push("独立构建");
logger_default.info(`- ${segments.join(",")}`);
}
}
const componentUsages = result.components ?? [];
if (componentUsages.length > 0) {
const suggestions = componentUsages.flatMap((component) => component.suggestions);
logger_default.info(`组件依赖:${componentUsages.length} 个组件,${suggestions.length} 条分包优化建议`);
for (const suggestion of suggestions.slice(0, 5)) logger_default.info(`- ${suggestion.message}`);
if (suggestions.length > 5) logger_default.info(`- …其余 ${suggestions.length - 5} 条组件建议请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
}
const duplicates = result.modules.filter((module) => module.packages.length > 1);
if (duplicates.length === 0) {
logger_default.info("未检测到跨包复用的源码模块。");
return;
}
logger_default.info(`跨包复用/复制源码共 ${duplicates.length} 项:`);
const limit = 10;
const entries = duplicates.slice(0, limit);
for (const module of entries) {
const placements = module.packages.map((pkgRef) => {
return `${packageLabelMap.get(pkgRef.packageId) ?? pkgRef.packageId} → ${pkgRef.files.join(", ")}`;
}).join(";");
logger_default.info(`- ${module.source} (${module.sourceType}):${placements}`);
}
if (duplicates.length > limit) logger_default.info(`- …其余 ${duplicates.length - limit} 项请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
}
function printBudgetCheckSummary(result) {
const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded");
if (exceededItems.length === 0) {
logger_default.success("包体预算检查通过");
return false;
}
logger_default.error(`包体预算检查失败:${exceededItems.length} 项超限`);
for (const item of exceededItems) logger_default.error(`- ${item.label}:${formatAnalyzeBytes(item.currentBytes)} / ${formatAnalyzeBytes(item.limitBytes)} (${(item.ratio * 100).toFixed(1)}%)`);
return true;
}
function printGlassEaselCheckSummary(result) {
const { glassEasel } = result;
if (!glassEasel.detected) {
logger_default.info("未检测到 glass-easel 配置。");
return false;
}
if (glassEasel.diagnostics.length === 0) {
logger_default.success("glass-easel 迁移检查通过");
return false;
}
logger_default.info(`glass-easel 迁移检查:${glassEasel.summary.errors} 个错误,${glassEasel.summary.warnings} 个警告`);
for (const diagnostic of glassEasel.diagnostics) {
const location = diagnostic.line ? `:${diagnostic.line}:${diagnostic.column}` : "";
const message = `[${diagnostic.code}] ${diagnostic.file}${location} ${diagnostic.message}`;
if (diagnostic.severity === "error") logger_default.error(message);
else logger_default.warn(message);
}
return glassEasel.summary.errors > 0;
}
function printWebAnalysisSummary(result) {
logger_default.success("Web 静态分析完成");
logger_default.info(`- 配置状态:${result.web.enabled ? "已启用 weapp.web" : "未启用 weapp.web"}`);
if (result.web.enabled) {
logger_default.info(`- root:${result.web.root ?? "."}`);
logger_default.info(`- srcDir:${result.web.srcDir ?? "."}`);
logger_default.info(`- outDir:${result.web.outDir ?? "dist/web"}`);
}
logger_default.info(`- executionMode:${result.web.executionMode}`);
logger_default.info(`- 支持范围:${result.supportedScopes.join(";")}`);
logger_default.warn(`- 未支持范围:${result.unsupportedScopes.join(";")}`);
for (const limitation of result.limitations) logger_default.warn(`- 限制:${limitation}`);
}
function printPreloadAnalysisSummary(result) {
logger_default.success("preloadRule 静态分析完成");
if (result.suggestions.length === 0) logger_default.info("未检测到可以证明的跨分包静态跳转。");
for (const suggestion of result.suggestions) {
const packages = suggestion.packages.join("、");
const covered = suggestion.alreadyConfigured.length > 0 ? `,已配置:${suggestion.alreadyConfigured.join("、")}` : "";
logger_default.info(`- ${suggestion.page} -> ${packages}${covered}`);
for (const evidence of suggestion.evidence.slice(0, 5)) logger_default.info(` - ${evidence.source}:${evidence.target}`);
}
if (result.uncoveredPages.length > 0) logger_default.warn(`- 未找到可扫描源码的页面:${result.uncoveredPages.join("、")}`);
if (result.budgets.length > 0) {
logger_default.info("预下载额度:");
for (const budget of result.budgets) {
const size = `${formatAnalyzeBytes(budget.estimatedBytes)} / ${formatAnalyzeBytes(budget.limitBytes)}`;
const unknown = budget.unknownPackages.length > 0 ? `,未知:${budget.unknownPackages.join("、")}` : "";
const status = budget.status === "exceeded" ? "超限" : budget.status === "unknown" ? "待确认" : "正常";
logger_default.info(`- ${budget.sourcePackage}:${size},${status}${unknown}`);
}
}
for (const limitation of result.limitations) logger_default.warn(`- 限制:${limitation}`);
}
async function writeAnalyzeResult(result, outputOption, configService, format = "json", previousResult) {
if (!outputOption) return;
const baseDir = configService.cwd;
const resolvedOutputPath = path.isAbsolute(outputOption) ? outputOption : path.resolve(baseDir, outputOption);
await fs$1.ensureDir(path.dirname(resolvedOutputPath));
const content = format === "markdown" && "packages" in result ? createAnalyzeMarkdownReport(result, previousResult) : format === "pr" && "packages" in result ? createAnalyzePrMarkdownReport(result, previousResult) : JSON.stringify(result, null, 2);
await fs$1.writeFile(resolvedOutputPath, `${content}\n`, "utf8");
const relativeOutput = configService.relativeCwd(resolvedOutputPath);
logger_default.success(`分析结果已写入 ${colors.green(relativeOutput)}`);
return resolvedOutputPath;
}
function formatMetricSummary(label, metric) {
if (!metric.count || metric.averageMs === void 0 || metric.maxMs === void 0) return;
return `${label} avg ${metric.averageMs.toFixed(2)} ms,max ${metric.maxMs.toFixed(2)} ms`;
}
function formatOperationSummary(label, operation) {
if (!operation.count || operation.average === void 0 || operation.max === void 0) return;
return `${label} avg ${operation.average.toFixed(1)},max ${operation.max.toFixed(0)}`;
}
function formatCountItems(items, limit = 5) {
return items.slice(0, limit).map((item) => `${item.name} x${item.count}`).join(",");
}
function printHmrProfileAnalysisSummary(result, configService) {
logger_default.success("HMR profile 分析完成");
logger_default.info(`- profile:${colors.green(configService.relativeCwd(result.profilePath))}`);
logger_default.info(`- 样本:${result.sampleCount} 条`);
if (result.firstTimestamp && result.lastTimestamp) logger_default.info(`- 时间范围:${result.firstTimestamp} -> ${result.lastTimestamp}`);
const totalSummary = formatMetricSummary("total", result.metrics.totalMs);
const buildStartSummary = formatMetricSummary("build-start", result.metrics.buildStartMs);
const pluginResolveSummary = formatMetricSummary("plugin-resolve", result.metrics.pluginResolveMs);
const coreLoadSummary = formatMetricSummary("core-load", result.metrics.coreLoadMs);
const entryLoadSummary = formatMetricSummary("entry-load", result.metrics.entryLoadMs);
const vueReadSourceSummary = formatMetricSummary("vue-read", result.metrics.vueReadSourceMs);
const vueCompileSummary = formatMetricSummary("vue-compile", result.metrics.vueCompileMs);
const vueFinalizeCompiledSummary = formatMetricSummary("vue-finalize-compiled", result.metrics.vueFinalizeCompiledMs);
const vueFinalizeCodeSummary = formatMetricSummary("vue-finalize-code", result.metrics.vueFinalizeCodeMs);
const entryEmitOutputSummary = formatMetricSummary("entry-emit-output", result.metrics.entryEmitOutputMs);
const entryTemplateScanSummary = formatMetricSummary("entry-template-scan", result.metrics.entryTemplateScanMs);
const entryAutoImportSummary = formatMetricSummary("entry-auto-import", result.metrics.entryAutoImportMs);
const entryScriptSetupSummary = formatMetricSummary("entry-script-setup", result.metrics.entryScriptSetupMs);
const entryVueSignatureSummary = formatMetricSummary("entry-vue-signature", result.metrics.entryVueSignatureMs);
const entrySidecarResolveSummary = formatMetricSummary("entry-sidecar-resolve", result.metrics.entrySidecarResolveMs);
const entryJsonReadSummary = formatMetricSummary("entry-json-read", result.metrics.entryJsonReadMs);
const entryVueConfigSummary = formatMetricSummary("entry-vue-config", result.metrics.entryVueConfigMs);
const entryPrepareSummary = formatMetricSummary("entry-prepare", result.metrics.entryPrepareMs);
const entryResolveSummary = formatMetricSummary("entry-resolve", result.metrics.entryResolveMs);
const entryStyleScanSummary = formatMetricSummary("entry-style-scan", result.metrics.entryStyleScanMs);
const entryStyleReadSummary = formatMetricSummary("entry-style-read", result.metrics.entryStyleReadMs);
const entryCodeReadSummary = formatMetricSummary("entry-code-read", result.metrics.entryCodeReadMs);
const entryChunkEmitSummary = formatMetricSummary("entry-chunk-emit", result.metrics.entryChunkEmitMs);
const entryChunkLoadSummary = formatMetricSummary("entry-chunk-load", result.metrics.entryChunkLoadMs);
const entryChunkEmitFileSummary = formatMetricSummary("entry-chunk-emit-file", result.metrics.entryChunkEmitFileMs);
const entryLayoutSummary = formatMetricSummary("entry-layout", result.metrics.entryLayoutMs);
const requestGlobalsSummary = formatMetricSummary("request-globals", result.metrics.requestGlobalsMs);
const weapiResolveSummary = formatMetricSummary("weapi-resolve", result.metrics.weapiResolveMs);
const snapshotResolveSummary = formatMetricSummary("snapshot-resolve", result.metrics.snapshotResolveMs);
const snapshotBuildSummary = formatMetricSummary("snapshot-build", result.metrics.snapshotBuildMs);
const writeSummary = formatMetricSummary("write", result.metrics.writeMs);
const watchSummary = formatMetricSummary("watch->dirty", result.metrics.watchToDirtyMs);
const emitSummary = formatMetricSummary("emit", result.metrics.emitMs);
const sharedSummary = formatMetricSummary("shared", result.metrics.sharedChunkResolveMs);
const chunkEmitSummary = formatOperationSummary("chunk emit", result.operations.chunkEmitCount);
const loadSummary = formatOperationSummary("load calls", result.operations.loadCount);
const resolveSummary = formatOperationSummary("resolve calls", result.operations.resolveCount);
const skippedLoadedSummary = formatOperationSummary("loaded skips", result.operations.skippedLoadedCount);
for (const summary of [
totalSummary,
buildStartSummary,
pluginResolveSummary,
coreLoadSummary,
entryLoadSummary,
vueReadSourceSummary,
vueCompileSummary,
vueFinalizeCompiledSummary,
vueFinalizeCodeSummary,
entryEmitOutputSummary,
entryTemplateScanSummary,
entryAutoImportSummary,
entryScriptSetupSummary,
entryVueSignatureSummary,
entrySidecarResolveSummary,
entryJsonReadSummary,
entryVueConfigSummary,
entryPrepareSummary,
entryResolveSummary,
entryStyleScanSummary,
entryStyleReadSummary,
entryCodeReadSummary,
entryChunkEmitSummary,
entryChunkLoadSummary,
entryChunkEmitFileSummary,
entryLayoutSummary,
requestGlobalsSummary,
weapiResolveSummary,
snapshotResolveSummary,
snapshotBuildSummary,
writeSummary,
watchSummary,
emitSummary,
sharedSummary
]) if (summary) logger_default.info(`- ${summary}`);
for (const summary of [
loadSummary,
resolveSummary,
chunkEmitSummary,
skippedLoadedSummary
]) if (summary) logger_default.info(`- ${summary}`);
if (result.events.length) logger_default.info(`- 事件分布:${formatCountItems(result.events)}`);
if (result.dirtyReasons.length) logger_default.info(`- 主要 dirty 原因:${formatCountItems(result.dirtyReasons)}`);
if (result.pendingReasons.length) logger_default.info(`- 主要 pending 原因:${formatCountItems(result.pendingReasons)}`);
if (result.skippedLineCount > 0) logger_default.warn(`- 跳过 ${result.skippedLineCount} 条无法解析的 profile 记录`);
if (result.slowestSamples.length) {
logger_default.info("- 最慢样本:");
for (const sample of result.slowestSamples.slice(0, 3)) {
const fileLabel = sample.file ? configService.relativeCwd(sample.file) : "(unknown)";
logger_default.info(` - ${sample.totalMs?.toFixed(2) ?? "0.00"} ms,${sample.event ?? "unknown"},${fileLabel}`);
}
}
}
function registerAnalyzeCommand(cli) {
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) => {
filterDuplicateOptions(options);
const configFile = resolveConfigFile(options);
const outputJson = coerceBooleanOption(options.json);
const outputMarkdown = coerceBooleanOption(options.markdown);
const reportType = typeof options.report === "string" ? options.report.trim() : "";
const outputPrReport = reportType === "pr";
if (reportType && !outputPrReport) throw new Error(`不支持的 analyze report 类型:${reportType}`);
const budgetCheck = coerceBooleanOption(options.budgetCheck);
const glassEaselCheck = coerceBooleanOption(options.glassEaselCheck);
const targets = resolveRuntimeTargets(options);
const inlineConfig = createInlineConfig(targets);
let ctx;
try {
ctx = await createCompilerContext({
cwd: root,
mode: options.mode ?? "production",
configFile,
inlineConfig,
cliPlatform: targets.rawPlatform,
projectConfigPath: options.projectConfig
});
logRuntimeTarget(targets, {
silent: outputJson || outputMarkdown,
resolvedConfigPlatform: ctx.configService.platform
});
const outputOption = typeof options.output === "string" ? options.output.trim() : "";
if (options.hmrProfile !== void 0 && options.hmrProfile !== false) {
const profileOption = typeof options.hmrProfile === "string" && options.hmrProfile.trim() ? options.hmrProfile.trim() : ctx.configService.weappViteConfig.hmr?.profileJson;
const profilePath = resolveHmrProfileJsonPath({
cwd: ctx.configService.cwd,
option: profileOption,
fallbackToDefault: true
});
if (!profilePath) throw new Error("未找到可用的 HMR profile 文件路径");
const hmrProfileResult = await analyzeHmrProfile({ profilePath });
const writtenPath = await writeAnalyzeResult(hmrProfileResult, outputOption, ctx.configService);
if (outputJson) {
if (!writtenPath) process.stdout.write(`${JSON.stringify(hmrProfileResult, null, 2)}\n`);
} else printHmrProfileAnalysisSummary(hmrProfileResult, ctx.configService);
return;
}
if (coerceBooleanOption(options.preload)) {
if (targets.kind !== "miniprogram" || ctx.configService.platform !== "weapp") throw new Error("preloadRule 分析目前仅支持微信小程序平台。");
const packageAnalysis = await analyzeSubpackages(ctx);
const preloadResult = await analyzePreloadRules(ctx, { packageAnalysis });
const writtenPath = await writeAnalyzeResult(preloadResult, outputOption, ctx.configService);
if (outputJson && !writtenPath) process.stdout.write(`${JSON.stringify(preloadResult, null, 2)}\n`);
if (!outputJson && !writtenPath) printPreloadAnalysisSummary(preloadResult);
return;
}
if (getBackendForCapability(targets, "web", "analyze")) {
const webResult = createWebAnalyzeResult(ctx.configService, { platform: "web" });
const writtenPath = await writeAnalyzeResult(webResult, outputOption, ctx.configService);
if (outputJson) {
if (!writtenPath) process.stdout.write(`${JSON.stringify(webResult, null, 2)}\n`);
} else printWebAnalysisSummary(webResult);
return;
}
if (!getBackendForCapability(targets, "miniprogram", "analyze")) {
logger_default.warn("当前命令不支持该平台,请通过 --platform weapp 或 --platform web 指定目标。");
return;
}
const previousResult = await readLatestAnalyzeHistorySnapshot(ctx.configService);
ctx.runtimeState.glassEasel.silent = Boolean(outputJson || outputMarkdown || outputPrReport || glassEaselCheck);
const result = await analyzeSubpackages(ctx);
await writeAnalyzeHistorySnapshot(result, ctx.configService);
const writtenPath = await writeAnalyzeResult(result, outputOption, ctx.configService, outputPrReport ? "pr" : outputMarkdown ? "markdown" : "json", previousResult);
if (outputPrReport) {
if (!writtenPath) process.stdout.write(`${createAnalyzePrMarkdownReport(result, previousResult)}\n`);
} else if (outputMarkdown) {
if (!writtenPath) process.stdout.write(`${createAnalyzeMarkdownReport(result, previousResult)}\n`);
} else if (outputJson) {
if (!writtenPath) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
}
const budgetFailed = budgetCheck ? printBudgetCheckSummary(result) : false;
const glassEaselFailed = glassEaselCheck && !outputJson ? printGlassEaselCheckSummary(result) : glassEaselCheck && result.glassEasel.summary.errors > 0;
if (budgetFailed || glassEaselFailed) process.exitCode = 1;
if (budgetCheck || glassEaselCheck) return;
if (!outputPrReport && !outputMarkdown && !outputJson) {
printAnalysisSummary(result);
await startAnalyzeDashboard(result, {
artifactRoot: ctx.configService.outDir,
cwd: ctx.configService.cwd,
packageManagerAgent: ctx.configService.packageManager.agent,
previousResult
});
}
} catch (error) {
logger_default.error(error);
process.exitCode = 1;
} finally {
if (ctx) {
for (const backend of [...targets.entries].reverse()) if (backend.descriptor.capabilities.analyze) await backend.driver?.close?.(ctx);
}
terminateStaleSassEmbeddedProcess();
}
});
}
//#endregion
//#region src/cli/formatDuration.ts
/**
* 将毫秒耗时格式化为适合 CLI 展示的文本。
*/
function formatDuration(durationMs) {
return `${durationMs}ms`;
}
//#endregion
//#region src/cli/logBuildAppFinish.ts
let logBuildAppFinishOnlyShowOnce = false;
function collectServerUrls(webServer) {
const urls = webServer?.resolvedUrls;
if (!urls) return [];
return [...urls.local ?? [], ...urls.network ?? []];
}
function logBuildAppFinish(configService, webServer, options = {}) {
if (logBuildAppFinishOnlyShowOnce) return;
const { skipMini = false, skipWeb = false, uiUrls = [] } = options;
const webUrls = skipWeb ? [] : collectServerUrls(webServer);
if (skipMini) {
logger_default.success("开发服务已就绪:");
if (webUrls.length > 0) logger_default.info(`Web:${colors.cyan(webUrls[0])}`);
else logger_default.info("Web:已启动");
logBuildAppFinishOnlyShowOnce = true;
return;
}
const { command, args } = resolveCommand(configService.packageManager.agent, "run", ["open"]) ?? {
command: "npm",
args: ["run", "open"]
};
const devCommand = `${command} ${args.join(" ")}`;
logger_default.success("开发服务已就绪:");
logger_default.info(`小程序:执行 ${colors.bold(colors.green(devCommand))},或手动导入 ${colors.green(getProjectConfigFileName(configService.platform))}`);
if (uiUrls.length > 0) logger_default.info(`UI:${colors.cyan(uiUrls[0])}`);
else if (!skipMini) logger_default.info("UI:未启用");
if (webUrls.length > 0) logger_default.info(`Web:${colors.cyan(webUrls[0])}`);
const projectConfigFileName = getProjectConfigFileName(configService.platform);
if (!uiUrls.length && !webUrls.length) logger_default.info(`提示:手动打开对应平台开发者工具,导入根目录(${colors.green(projectConfigFileName)} 文件所在目录)`);
logBuildAppFinishOnlyShowOnce = true;
}
const WINDOWS_SEPARATOR_RE = /\\/g;
function normalizeFileName(fileName) {
return fileName.replace(WINDOWS_SEPARATOR_RE, "/");
}
function getOutputItemBytes(item) {
if (item.type === "chunk") return typeof item.code === "string" ? Buffer.byteLength(item.code, "utf8") : 0;
if (typeof item.source === "string") return Buffer.byteLength(item.source, "utf8");
if (item.source instanceof Uint8Array) return item.source.byteLength;
return 0;
}
function resolveSubPackageRoot(fileName, roots) {
const normalized = normalizeFileName(fileName);
return roots.find((root) => normalized === root || normalized.startsWith(`${root}/`));
}
function collectPackageSizeReports(output, subPackageMap) {
const outputs = Array.isArray(output) ? output : [output];
const roots = [...subPackageMap?.keys() ?? []].filter(Boolean).sort((a, b) => b.length - a.length || a.localeCompare(b));
const packageBytes = /* @__PURE__ */ new Map([["__main__", 0]]);
for (const root of roots) packageBytes.set(root, 0);
for (const current of outputs) for (const item of current.output ?? []) {
const root = resolveSubPackageRoot(item.fileName, roots) ?? "__main__";
packageBytes.set(root, (packageBytes.get(root) ?? 0) + getOutputItemBytes(item));
}
const reports = [{
root: "__main__",
label: "主包",
bytes: packageBytes.get("__main__") ?? 0
}];
for (const root of roots) {
const meta = subPackageMap?.get(root);
const isIndependent = Boolean(meta?.subPackage.independent);
reports.push({
root,
label: `${isIndependent ? "独立分包" : "分包"} ${root}`,
bytes: packageBytes.get(root) ?? 0
});
}
return reports;
}
function logBuildPackageSizeReport(options) {
const warningBytes = Number(options.warningBytes ?? 2097152);
const reports = collectPackageSizeReports(options.output, options.subPackageMap);
logger_default.success("主包/分包体积报告:");
for (const report of reports) logger_default.info(`${report.label}:${formatBytes(report.bytes)}`);
if (!(Number.isFinite(warningBytes) && warningBytes > 0)) return;
for (const report of reports) {
if (report.bytes <= warningBytes) continue;
logger_default.warn(`[包体积] ${colors.yellow(report.label)} 体积 ${colors.yellow(formatBytes(report.bytes))},已超过阈值 ${colors.yellow(formatBytes(warningBytes))}。`);
}
}
//#endregion
//#region src/cli/nodeEnv.ts
function setCommandNodeEnv(nodeEnv) {
process.env.NODE_ENV = nodeEnv;
}
//#endregion
//#region src/cli/commands/build.ts
function emitDashboardEvents$1(handle, events) {
handle?.emitRuntimeEvents(events);
}
function normalizeBuildSourcemap(value) {
if (value === "true") return true;
if (value === "false") return false;
return value;
}
function normalizeBuildMinify(value) {
if (value === "true") return true;
if (value === "false") return false;
return value;
}
function hasCliOption(options, key) {
return Object.prototype.hasOwnProperty.call(options, key);
}
function createBuildInlineConfig(options) {
const build = {};
if (typeof options.outDir === "string") build.outDir = options.outDir;
if (hasCliOption(options, "sourcemap")) build.sourcemap = normalizeBuildSourcemap(options.sourcemap);
if (hasCliOption(options, "minify")) build.minify = normalizeBuildMinify(options.minify);
if (typeof options.emptyOutDir === "boolean") build.emptyOutDir = options.emptyOutDir;
return Object.keys(build).length ? { build } : void 0;
}
function shouldScheduleCompletedProductionBuildExit(options, analyzeHandle) {
return process.env.VITEST !== "true" && process.env.NODE_ENV !== "test" && process.env.WEAPP_VITE_DISABLE_COMPLETED_BUILD_EXIT !== "1" && options.watch !== true && options.open !== true && analyzeHandle === void 0;
}
function scheduleCompletedProductionBuildExit(options, analyzeHandle) {
if (!shouldScheduleCompletedProductionBuildExit(options, analyzeHandle)) return;
setTimeout(() => {
if (process.exitCode === void 0 || process.exitCode === 0) process.exit(0);
}, 0).unref?.();
}
function registerBuildCommand(cli) {
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) => {
let analyzeHandle;
let ctx;
let targets;
let buildCompleted = false;
try {
filterDuplicateOptions(options);
setCommandNodeEnv("production");
const cwd = root ?? process.cwd();
const configFile = resolveConfigFile(options);
targets = resolveRuntimeTargets(options);
const inlineConfig = createInlineConfig(targets, {
scope: options.scope,
inlineConfig: createBuildInlineConfig(options)
});
ctx = await createCompilerContext({
cwd,
mode: options.mode ?? "production",
configFile,
inlineConfig,
cliPlatform: targets.rawPlatform,
projectConfigPath: options.projectConfig,
emitDefaultAutoImportOutputs: false,
preloadAppEntry: false
});
const { configService } = ctx;
const miniBackend = getBackendForCapability(targets, "miniprogram", "build");
const webBackend = getBackendForCapability(targets, "web", "build");
logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
const enableAnalyze = Boolean(isUiEnabled(options) && miniBackend);
for (const backend of targets.select("build")) {
if (backend.descriptor.id === "miniprogram") {
const miniBuildStartedAt = Date.now();
const output = await backend.driver.build(ctx, options);
const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
logger_default.success(`小程序构建完成,耗时:${colors.green(formatDuration(miniBuildDurationMs))}`);
if (!Array.isArray(output) && "output" in output) logBuildPackageSizeReport({
output,
subPackageMap: ctx.scanService?.subPackageMap,
warningBytes: configService.weappViteConfig.packageSizeWarningBytes
});
if (enableAnalyze) {
const analyzeStartedAt = Date.now();
const previousAnalyzeResult = await readLatestAnalyzeHistorySnapshot(configService);
const analyzeResult = await analyzeSubpackages(ctx);
await writeAnalyzeHistorySnapshot(analyzeResult, configService);
const analyzeDurationMs = Date.now() - analyzeStartedAt;
analyzeHandle = await startAnalyzeDashboard(analyzeResult, {
watch: true,
artifactRoot: configService.outDir,
cwd: configService.cwd,
packageManagerAgent: configService.packageManager.agent,
previousResult: previousAnalyzeResult,
initialEvents: [{
kind: "build",
level: "success",
title: "mini build completed",
detail: `生产构建已完成,当前 analyze 结果包含 ${analyzeResult.packages.length} 个包。`,
durationMs: miniBuildDurationMs,
tags: ["build", "mini"]
}, {
kind: "build",
level: "success",
title: "analyze completed",
detail: `分析已完成,当前包含 ${analyzeResult.packages.length} 个包与 ${analyzeResult.modules.length} 个模块。`,
durationMs: analyzeDurationMs,
tags: ["build", "analyze"]
}]
}) ?? void 0;
}
continue;
}
const webConfig = configService.weappWebConfig;
if (backend.descriptor.id !== "web" || !webConfig?.enabled) continue;
const webBuildStartedAt = Date.now();
try {
await backend.driver.build(ctx, options);
const webBuildDurationMs = Date.now() - webBuildStartedAt;
logger_default.success(`Web 构建完成,输出目录:${colors.green(configService.relativeCwd(webConfig.outDir))},耗时:${colors.green(formatDuration(webBuildDurationMs))}`);
emitDashboardEvents$1(analyzeHandle, [{
kind: "build",
level: "success",
title: "web build completed",
detail: `Web 构建已完成,输出目录 ${configService.relativeCwd(webConfig.outDir)}。`,
durationMs: webBuildDurationMs,
tags: ["build", "web"]
}]);
} catch (error) {
emitDashboardEvents$1(analyzeHandle, [{
kind: "diagnostic",
level: "error",
title: "web build failed",
detail: error instanceof Error ? error.message : String(error),
durationMs: Date.now() - webBuildStartedAt,
tags: ["build", "web"]
}]);
logger_default.error(error);
throw error;
}
}
if (miniBackend) logBuildAppFinish(configService, void 0, { skipWeb: !webBackend });
if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
emitDashboardEvents$1(analyzeHandle, [{
kind: "command",
level: "info",
title: "opening ide",
detail: "构建完成后准备打开 IDE 项目。",
tags: ["ide", "open"]
}]);
await openIde(configService.platform, resolveIdeProjectPath(configService.mpDistRoot), {
openRecovery: options.openRecovery,
trustProject: options.trustProject,
openStrategy: options.ideOpenStrategy
});
}
if (analyzeHandle) await analyzeHandle.waitForExit();
buildCompleted = true;
} finally {
if (ctx && targets) for (const backend of [...targets.select("build")].reverse()) await backend.driver.close(ctx);
terminateStaleSassEmbeddedProcess();
if (buildCompleted) scheduleCompletedProductionBuildExit(options, analyzeHandle);
}
});
}
//#endregion
//#region src/cli/commands/close.ts
function registerCloseCommand(cli) {
cli.command("close", "close Wechat DevTools").action(async () => {
if (!await closeIde()) throw new Error("关闭微信开发者工具失败。");
});
}
//#endregion
//#region src/schematics.ts
function composePath(outDir, filename) {
return `${outDir}${outDir ? "/" : ""}${filename}`;
}
const defaultExtensions = {
js: "js",
json: "json",
wxml: "wxml",
wxss: "wxss"
};
function resolveExtension(extension) {
return extension ? extension.startsWith(".") ? extension : `.${extension}` : "";
}
async function readTemplateFile(templatePath, context) {
const absolutePath = path.isAbsolute(templatePath) ? templatePath : path.resolve(context.cwd, templatePath);
return fs$1.readFile(absolutePath, "utf8");
}
async function loadTemplate(template, context) {
if (template === void 0) return;
if (typeof template === "function") {
const result = await template(context);
return result == null ? void 0 : String(result);
}
if (typeof template === "string") return readTemplateFile(template, context);
if ("content" in template && typeof template.content === "string") return template.content;
if ("path" in template && typeof template.path === "string") return readTemplateFile(template.path, context);
}
function resolveTemplate(templates, type, fileType) {
const scoped = templates?.[type]?.[fileType];
if (scoped !== void 0) return scoped;
return templates?.shared?.[fileType];
}
async function generate(options) {
let { fileName, outDir, extensions, type, cwd, templates } = defu(options, {
type: "component",
extensions: { ...defaultExtensions },
cwd: process.cwd(),
templates: void 0
});
if (fileName === void 0) fileName = path.basename(outDir);
const basepath = path.resolve(cwd, outDir);
const targetFileTypes = type === "app" ? [
"js",
"wxss",
"json"
] : [
"js",
"wxss",
"json",
"wxml"
];
const files = [];
for (const fileType of targetFileTypes) {
const configuredExt = extensions[fileType] ?? defaultExtensions[fileType];
let resolvedExt = configuredExt;
let defaultCode;
if (fileType === "js") defaultCode = generateJs(type);
else if (fileType === "wxss") defaultCode = generateWxss();
else if (fileType === "wxml") defaultCode = generateWxml(path.join(outDir, fileName));
else if (fileType === "json") {
defaultCode = generateJson(type, configuredExt);
if (configuredExt === "js" || configuredExt === "ts") resolvedExt = `json.${configuredExt}`;
}
const context = {
type,
fileType,
fileName,
outDir,
extension: resolvedExt,
cwd,
defaultCode
};
const finalCode = await loadTemplate(resolveTemplate(templates, type, fileType), context) ?? defaultCode;
if (finalCode !== void 0) files.push({
fileName: `${fileName}${resolveExtension(resolvedExt)}`,
code: finalCode
});
}
for (const { code, fileName } of files) if (code !== void 0) {
await fs$1.outputFile(path.resolve(basepath, fileName), code, "utf8");
logger_default.success(`${composePath(outDir, fileName)} 创建成功!`);
}
}
//#endregion
//#region src/cli/loadConfig.ts
async function loadConfig(configFile) {
const cwd = process.cwd();
let resolvedConfigFile = configFile;
if (resolvedConfigFile && !path.isAbsolute(resolvedConfigFile)) resolvedConfigFile = path.resolve(cwd, resolvedConfigFile);
const configEnv = {
command: "serve",
mode: "development"
};
let loaded;
try {
loaded = await loadViteConfigFile(configEnv, resolvedConfigFile, cwd, void 0, void 0, "runner");
} catch (error) {
const cjsError = createCjsConfigLoadError({
error,
configPath: resolvedConfigFile,
cwd
});
if (cjsError) throw cjsError;
throw error;
}
const weappConfigFilePath = await resolveWeappConfigFile({
root: cwd,
specified: resolvedConfigFile
});
let weappLoaded;
if (weappConfigFilePath) {
const normalizedWeappPath = path.resolve(weappConfigFilePath);
const normalizedLoadedPath = loaded?.path ? path.resolve(loaded.path) : void 0;
if (normalizedLoadedPath && normalizedLoadedPath === normalizedWeappPath) weappLoaded = loaded;
else try {
weappLoaded = await loadViteConfigFile(configEnv, weappConfigFilePath, cwd, void 0, void 0, "runner");
} catch (error) {
const cjsError = createCjsConfigLoadError({
error,
configPath: weappConfigFilePath,
cwd
});
if (cjsError) throw cjsError;
throw error;
}
}
if (!loaded && !weappLoaded) return;
const config = weappLoaded?.config ? defu(weappLoaded.config, loaded?.config ?? {}) : loaded?.config ?? {};
const dependencySet = /* @__PURE__ */ new Set();
for (const dependency of loaded?.dependencies ?? []) dependencySet.add(dependency);
for (const dependency of weappLoaded?.dependencies ?? []) dependencySet.add(dependency);
return {
config,
path: weappLoaded?.path ?? loaded?.path ?? resolvedConfigFile,
dependencies: Array.from(dependencySet)
};
}
//#endregion
//#region src/cli/commands/generate.ts
function registerGenerateCommand(cli) {
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) => {
filterDuplicateOptions(options);
const config = await loadConfig(resolveConfigFile(options));
let type = "component";
let fileName = options.name;
if (options.app) {
type = "app";
if (filepath === void 0) filepath = "";
fileName = "app";
}
if (filepath === void 0) {
logger_default.error("weapp-vite generate <outDir> 命令必须传入路径参数 outDir");
return;
}
if (options.page) type = "page";
const generateOptions = config?.config.weapp?.generate;
fileName = generateOptions?.filenames?.[type] ?? fileName;
await generate({
outDir: path.join(generateOptions?.dirs?.[type] ?? "", filepath),
type,
fileName,
extensions: generateOptions?.extensions,
templates: generateOptions?.templates
});
});
}
//#endregion
//#region src/cli/forwardConsole.ts
const DEFAULT_FORWARD_CONSOLE_LEVELS = [
"log",
"info",
"warn",
"error"
];
let activeForwardConsoleSession;
let activeForwardConsoleBridgeOptions;
let activeForwardConsoleStart;
let forwardConsoleLifecycle = 0;
const FORWARD_CONSOLE_RETRY_DELAY_MS = 1e3;
const FORWARD_CONSOLE_RETRY_TIMES = 5;
const FORWARD_CONSOLE_RETRY_WINDOW_MS = 1e4;
const FORWARD_CONSOLE_RECOVERABLE_START_TIMEOUT_MS = 6e4;
const FORWARD_CONSOLE_START_TIMEOUT_MS = 12e4;
async function detectAgent() {
try {
const result = await determineAgent();
return {
isAgent: result.isAgent,
agentName: result.isAgent ? result.agent.name : void 0
};
} catch {
return {
isAgent: false,
agentName: void 0
};
}
}
function formatForwardConsolePrefix(level, color) {
const label = `[mini:${level.padEnd(5)}]`;
if (!color) return label;
if (level === "error") return colors.bold(colors.red(label));
if (level === "warn") return colors.bold(colors.yellow(label));
if (level === "info") return colors.bold(colors.cyan(label));
if (level === "debug") return colors.dim(label);
return colors.bold(colors.green(label));
}
function formatForwardConsoleMessage(level, message, color) {
if (!color) return message;
if (level === "error") return colors.red(message);
if (level === "warn") return colors.yellow(message);
if (level === "debug") return colors.dim(message);
return message;
}
function writeForwardConsoleLine(line) {
process.stdout.write(`${line}\n`);
}
function isDevtoolsPortNotReadyError(error) {
return error instanceof Error && (error.message === "DEVTOOLS_HTTP_PORT_ERROR" || error.message === "DEVTOOLS_WS_CONNECT_ERROR" || error.message === "DEVTOOLS_EXTENSION_CONTEXT_INVALIDATED");
}
function isDevtoolsProtocolTimeoutError(error) {
return error instanceof Error && (error.message === "DEVTOOLS_PROTOCOL_TIMEOUT" || /DevTools did not respond to protocol method \S+ within \d+ms/i.test(error.message));
}
function isRecoverableAutomatorStartError(error) {
return isDevtoolsPortNotReadyError(error) || isDevtoolsProtocolTimeoutError(error) || isRetryableAutomatorLaunchError(error);
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function withForwardConsoleRetry(runner) {
let lastError;
const startedAt = Date.now();
for (let attempt = 0; attempt <= FORWARD_CONSOLE_RETRY_TIMES; attempt++) try {
return await runner();
} catch (error) {
lastError = error;
if (!isDevtoolsPortNotReadyError(error) || attempt === FORWARD_CONSOLE_RETRY_TIMES || Date.now() - startedAt >= FORWARD_CONSOLE_RETRY_WINDOW_MS) break;
await sleep(FORWARD_CONSOLE_RETRY_DELAY_MS);
}
throw lastError instanceof Error ? lastError : new Error(String(lastError));
}
/**
* @description 解析 forwardConsole 配置,并在 auto 模式下检测 AI 终端。
*/
async function resolveForwardConsoleOptions(config) {
const rawConfig = config?.forwardConsole;
const defaults = {
logLevels: DEFAULT_FORWARD_CONSOLE_LEVELS,
unhandledErrors: true
};
if (rawConfig === false) return {
enabled: false,
...defaults
};
if (rawConfig === true) return {
enabled: true,
...defaults
};
const normalizedConfig = rawConfig ?? {};
const enabledMode = normalizedConfig.enabled ?? "auto";
const logLevels = normalizedConfig.logLevels?.length ? normalizedConfig.logLevels : defaults.logLevels;
const unhandledErrors = normalizedConfig.unhandledErrors ?? defaults.unhandledErrors;
if (enabledMode === true) return {
enabled: true,
logLevels,
unhandledErrors
};
if (enabledMode === false) return {
enabled: false,
logLevels,
unhandledErrors
};
const agentResult = await detectAgent();
return {
enabled: agentResult.isAgent,
agentName: agentResult.agentName,
logLevels,
unhandledErrors
};
}
/**
* @description 统一启动 DevTools 日志桥,并在 IDE 刚启动时做短暂重试。
*/
async function startForwardConsoleBridge(options) {
return await withForwardConsoleRetry(async () => {
const color = options.color ?? true;
return await startForwardConsole({
projectPath: options.projectPath,
logLevels: options.logLevels,
openedOnly: options.openedOnly,
preferOpenedSession: options.preferOpenedSession,
preserveProjectRoot: options.preserveProjectRoot,
port: options.port,
timeout: options.timeout,
unhandledErrors: options.unhandledErrors,
onReady: () => {
const suffix = options.agentName ? `(AI 终端:${options.agentName})` : "";
logger_default.info(`${options.onReadyMessage}${suffix}`);
},
onLog: (event) => {
writeForwardConsoleLine(`${formatForwardConsolePrefix(event.level, color)} ${formatForwardConsoleMessage(event.level, event.message, color)}`);
}
});
});
}
/**
* @description 暂停当前 DevTools 日志桥,并返回恢复函数。
*/
async function pauseActiveForwardConsole() {
const lifecycle = forwardConsoleLifecycle;
await activeForwardConsoleStart;
if (lifecycle !== forwardConsoleLifecycle) return;
const session = activeForwardConsoleSession;
const bridgeOptions = activeForwardConsoleBridgeOptions;
if (!session || !bridgeOptions) return;
activeForwardConsoleSession = void 0;
activeForwardConsoleBridgeOptions = void 0;
await session.close();
return async () => {
if (lifecycle !== forwardConsoleLifecycle) return false;
if (activeForwardConsoleStart) return await activeForwardConsoleStart;
if (activeForwardConsoleSession) return true;
const startTask = (async () => {
try {
const resumedSession = await startForwardConsoleBridge(bridgeOptions);
if (lifecycle !== forwardConsoleLifecycle) {
await resumedSession.close();
return false;
}
activeForwardConsoleSession = resumedSession;
activeForwardConsoleBridgeOptions = bridgeOptions;
return true;
} catch (error) {
if (lifecycle === forwardConsoleLifecycle) {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[forwardConsole] 恢复失败:${message}`);
}
return false;
}
})();
activeForwardConsoleStart = startTask;
try {
return await startTask;
} finally {
if (activeForwardConsoleStart === startTask) activeForwardConsoleStart = void 0;
}
};
}
/**
* @description 关闭当前 DevTools 日志桥并释放共享 automator 会话。
*/
async function closeActiveForwardConsole() {
forwardConsoleLifecycle += 1;
const session = activeForwardConsoleSession;
const pendingStart = activeForwardConsoleStart;
activeForwardConsoleSession = void 0;
activeForwardConsoleBridgeOptions = void 0;
await session?.close();
await pendingStart?.catch(() => {});
}
/**
* @description 在 weapp 开发态按需启动控制台转发。
*/
async function maybeStartForwardConsole(options) {
if (options.platform !== "weapp") return false;
const projectPath = resolveIdeProjectPath(options.mpDistRoot) ?? options.cwd;
if (!projectPath) return false;
if (activeForwardConsoleSession) return true;
if (activeForwardConsoleStart) return await activeForwardConsoleStart;
const lifecycle = forwardConsoleLifecycle;
const startTask = (async () => {
const resolved = await resolveForwardConsoleOptions(options.weappViteConfig);
if (!resolved.enabled || lifecycle !== forwardConsoleLifecycle) return false;
const bridgeOptions = {
agentName: resolved.agentName,
color: !resolved.agentName,
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: options.recoverAutomatorSession ? FORWARD_CONSOLE_RECOVERABLE_START_TIMEOUT_MS : FORWARD_CONSOLE_START_TIMEOUT_MS,
logLevels: resolved.logLevels,
openedOnly: options.openedOnly,
preferOpenedSession: options.preferOpenedSession,
preserveProjectRoot: options.preserveProjectRoot ?? !shouldUseAutomatorProjectWrapper(projectPath),
unhandledErrors: resolved.unhandledErrors,
onReadyMessage: "[forwardConsole] 已连接微信开发者工具日志"
};
const activateSession = async (session, activeOptions) => {
if (lifecycle !== forwardConsoleLifecycle) {
await session.close();
return false;
}
activeForwardConsoleSession = session;
activeForwardConsoleBridgeOptions = activeOptions;
return true;
};
const recoverAutomatorSession = async () => {
try {
await options.recoverAutomatorSession?.();
if (lifecycle !== forwardConsoleLifecycle) return false;
const recoveredOptions = {
...bridgeOptions,
openedOnly: true,
preferOpenedSession: true
};
return await activateSession(await startForwardConsoleBridge(recoveredOptions), recoveredOptions);
} catch (recoveryError) {
if (lifecycle !== forwardConsoleLifecycle) return false;
const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError);
logger_default.warn(`[forwardConsole] automator 会话恢复失败,回退到普通 IDE 打开流程:${message}`);
return false;
}
};
try {
return await activateSession(await startForwardConsoleBridge(bridgeOptions), bridgeOptions);
} catch (error) {
if (lifecycle !== forwardConsoleLifecycle) return false;
if (options.recoverAutomatorSession && isRecoverableAutomatorStartError(error)) return await recoverAutomatorSession();
if (!isDevtoolsPortNotReadyError(error)) {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[forwardConsole] 启动失败,回退到普通 IDE 打开流程:${message}`);
return false;
}
const fallbackOptions = {
...bridgeOptions,
openedOnly: true,
port: void 0,
preferOpenedSession: true
};
try {
return await activateSession(await startForwardConsoleBridge(fallbackOptions), fallbackOptions);
} catch (fallbackError) {
if (lifecycle !== forwardConsoleLifecycle) return false;
const message = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
logger_default.warn(`[forwardConsole] 启动失败,回退到普通 IDE 打开流程:${message}`);
return false;
}
}
})();
activeForwardConsoleStart = startTask;
try {
return await startTask;
} finally {
if (activeForwardConsoleStart === startTask) activeForwardConsoleStart = void 0;
}
}
//#endregion
//#region src/cli/hmrProfileSummary.ts
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function parseLatestHmrProfileSample(content) {
const lines = content.split(/\r?\n/);
for (let index = lines.length - 1; index >= 0; index -= 1) {
const trimmed = lines[index]?.trim();
if (!trimmed) continue;
try {
const parsed = JSON.parse(trimmed);
if (isFiniteNumber(parsed.totalMs)) return parsed;
} catch {
continue;
}
}
}
function formatPhaseHint(sample) {
const topPhase = [
{
label: "build-core",
value: sample.buildCoreMs
},
{
label: "transform",
value: sample.transformMs
},
{
label: "build-start",
value: sample.buildStartMs
},
{
label: "plugin-resolve",
value: sample.pluginResolveMs
},
{
label: "core-transform",
value: sample.coreTransformMs
},
{
label: "wevu-transform",
value: sample.wevuTransformMs
},
{
label: "vue-transform",
value: sample.vueTransformMs
},
{
label: "vue-read",
value: sample.vueReadSourceMs
},
{
label: "vue-compile",
value: sample.vueCompileMs
},
{
label: "vue-finalize-compiled",
value: sample.vueFinalizeCompiledMs
},
{
label: "vue-finalize-code",
value: sample.vueFinalizeCodeMs
},
{
label: "core-load",
value: sample.coreLoadMs
},
{
label: "entry-load",
value: sample.entryLoadMs
},
{
label: "entry-emit-output",
value: sample.entryEmitOutputMs
},
{
label: "entry-template-scan",
value: sample.entryTemplateScanMs
},
{
label: "entry-auto-import",
value: sample.entryAutoImportMs
},
{
label: "entry-script-setup",
value: sample.entryScriptSetupMs
},
{
label: "entry-vue-signature",
value: sample.entryVueSignatureMs
},
{
label: "entry-sidecar-resolve",
value: sample.entrySidecarResolveMs
},
{
label: "entry-json-read",
value: sample.entryJsonReadMs
},
{
label: "entry-vue-config",
value: sample.entryVueConfigMs
},
{
label: "entry-prepare",
value: sample.entryPrepareMs
},
{
label: "entry-resolve",
value: sample.entryResolveMs
},
{
label: "entry-style-scan",
value: sample.entryStyleScanMs
},
{
label: "entry-style-read",
value: sample.entryStyleReadMs
},
{
label: "entry-code-read",
value: sample.entryCodeReadMs
},
{
label: "entry-chunk-emit",
value: sample.entryChunkEmitMs
},
{
label: "entry-chunk-load",
value: sample.entryChunkLoadMs
},
{
label: "entry-chunk-emit-file",
value: sample.entryChunkEmitFileMs
},
{
label: "entry-layout",
value: sample.entryLayoutMs
},
{
label: "request-globals",
value: sample.requestGlobalsMs
},
{
label: "weapi-resolve",
value: sample.weapiResolveMs
},
{
label: "render-start",
value: sample.renderStartMs
},
{
label: "generate",
value: sample.generateBundleMs
},
{
label: "generate-shared",
value: sample.generateSharedMs
},
{
label: "generate-rewrite",
value: sample.generateRewriteMs
},
{
label: "module-graph",
value: sample.generateModuleGraphMs
},
{
label: "snapshot-resolve",
value: sample.snapshotResolveMs
},
{
label: "snapshot-build",
value: sample.snapshotBuildMs
},
{
label: "watch->dirty",
value: sample.watchToDirtyMs
},
{
label: "emit",
value: sample.emitMs
},
{
label: "shared",
value: sample.sharedChunkResolveMs
},
{
label: "write",
value: sample.writeMs
}
].filter((phase) => isFiniteNumber(phase.value)).sort((left, right) => (right.value ?? 0) - (left.value ?? 0))[0];
if (!topPhase || (topPhase.value ?? 0) < 5) return;
return `${topPhase.label} ${topPhase.value.toFixed(2)} ms`;
}
/**
* @description 读取最近一次 HMR profile,并格式化为 IDE 日志启动前的单行摘要。
*/
async function readLatestHmrProfileSummary(options) {
const profilePath = resolveHmrProfileJsonPath({
cwd: options.cwd,
option: options.weappViteConfig?.hmr?.profileJson
});
if (!profilePath) return;
const content = await fs$1.readFile(profilePath, "utf8").catch(() => void 0);
if (!content) return;
const sample = parseLatestHmrProfileSample(content);
if (!sample || !isFiniteNumber(sample.totalMs)) return;
const relativeCwd = options.relativeCwd ?? ((value) => value);
const segments = [`[hmr] 最近一次热更新 ${sample.totalMs.toFixed(2)} ms`];
if (sample.event) segments.push(sample.event);
if (sample.file) segments.push(relativeCwd(sample.file));
const phaseHint = formatPhaseHint(sample);
if (phaseHint) segments.push(`主耗时 ${phaseHint}`);
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}`);
return {
file: sample.file,
profilePath,
line: segments.join(",")
};
}
//#endregion
//#region src/cli/commands/ide.ts
async function runIdeDoctor(projectPath, options) {
const resolvedCli = await resolveCliPath();
const servicePort = await detectWechatDevtoolsServicePort().then((result) => ({
...result,
error: void 0
})).catch((error) => ({
servicePortEnabled: void 0,
servicePort: void 0,
error
}));
const cliCheck = resolvedCli.cliPath ? {
status: "ok",
value: {
path: resolvedCli.cliPath,
source: resolvedCli.source
}
} : {
fix: "执行 `weapp config set cliPath <path-to-cli>` 配置微信开发者工具 CLI。",
message: "未找到可用的微信开发者工具 CLI。",
status: "failed",
value: { source: resolvedCli.source }
};
const servicePortCheck = servicePort.servicePortEnabled === false ? {
fix: "在微信开发者工具中打开:设置 -> 安全设置 -> 服务端口。",
message: "微信开发者工具服务端口已关闭。",
status: "failed",
value: servicePort
} : servicePort.error ? {
fix: "执行 `wv ide doctor --json` 查看底层错误,并确认微信开发者工具已安装。",
message: servicePort.error instanceof Error ? servicePort.error.message : String(servicePort.error),
status: "warning"
} : {
status: servicePort.servicePortEnabled === true ? "ok" : "unknown",
value: {
enabled: servicePort.servicePortEnabled ?? null,
port: servicePort.servicePort ?? null
}
};
let loginCheck = {
fix: "打开微信开发者工具完成登录后重试。",
message: "暂未确认登录状态。",
status: "unknown"
};
if (resolvedCli.cliPath) try {
await isWechatIdeLoggedIn({
nonInteractive: true,
silent: true
});
loginCheck = {
status: "ok",
value: true
};
} catch (error) {
loginCheck = {
fix: "打开微信开发者工具完成登录后重试,或执行 `wv ide doctor --json` 查看 CLI 错误。",
message: error instanceof Error ? error.message : String(error),
status: "warning",
value: false
};
}
const projectCheck = projectPath ? {
status: "ok",
value: projectPath
} : {
fix: "在项目目录执行命令,或显式传入 `wv ide doctor <project-root>`。",
message: "未解析到微信开发者工具项目目录。",
status: "warning"
};
let automatorCheck = {
message: projectPath ? "当前项目没有可连接的已打开 automator 会话。" : "缺少项目目录,跳过 automator 检查。",
status: "unknown"
};
let toolCheck = {
message: "当前没有可读取的 DevTools Tool.getInfo。",
status: "unknown"
};
if (projectPath) try {
const miniProgram = await connectOpenedAutomator({
projectPath,
port: resolveProjectAutomatorPort(projectPath),
timeout: 3e3
});
automatorCheck = {
status: "ok",
value: true
};
if (typeof miniProgram.toolInfo === "function") toolCheck = {
status: "ok",
value: await miniProgram.toolInfo()
};
miniProgram.disconnect?.();
} catch (error) {
automatorCheck = {
fix: "先执行 `wv open` 打开项目;若服务端口已开启,关闭多余 DevTools 窗口后重试。",
message: error instanceof Error ? error.message : String(error),
status: "warning"
};
}
const report = {
checks: {
automator: automatorCheck,
cli: cliCheck,
login: loginCheck,
project: projectCheck,
servicePort: servicePortCheck,
tool: toolCheck
},
configFile: defaultCustomConfigFilePath,
nodeVersion: process.version,
platform: process.platform,
weappViteVersion: VERSION
};
const output = JSON.stringify(report, null, 2);
if (options.json) process.stdout.write(`${output}\n`);
else logger_default.info(output);
if (options.strict && Object.values(report.checks).some((check) => check.status === "failed")) throw new Error("微信开发者工具环境诊断未通过,请根据 doctor 输出修复失败项后重试。");
return report;
}
async function waitForTermination(cleanup) {
await new Promise((resolve) => {
const signals = ["SIGINT", "SIGTERM"];
let cleaning = false;
const handlers = /* @__PURE__ */ new Map();
const teardown = async () => {
if (cleaning) return;
cleaning = true;
for (const signal of signals) process.off(signal, handlers.get(signal));
await cleanup();
resolve();
};
for (const signal of signals) {
const handler = () => {
teardown();
};
handlers.set(signal, handler);
process.on(signal, handler);
}
});
}
function formatIdeOutput(data, options) {
if (options.json) return JSON.stringify(data, null, 2);
return JSON.stringify(data, null, 2);
}
/**
* @description 执行 ide 子命令。
*/
async function runIdeCommand(action, root, options) {
if (action !== "logs" && action !== "setup" && action !== "info" && action !== "test-accounts" && action !== "ticket" && action !== "ticket:set" && action !== "ticket:refresh" && action !== "doctor") throw new Error(`未知 ide 子命令: ${action ?? "(empty)"}`);
filterDuplicateOptions(options);
const configFile = resolveConfigFile(options);
const targets = resolveRuntimeTargets(options);
const ideBackend = getBackendForCapability(targets, "miniprogram", "ide");
if (!ideBackend) throw new Error("`weapp-vite ide` 当前仅支持小程序平台。");
const resolved = await resolveIdeCommandContext({
configFile,
mode: options.mode ?? "development",
platform: ideBackend.platform,
projectPath: root,
cliPlatform: targets.rawPlatform
});
if (action !== "doctor" && resolved.platform !== "weapp") throw new Error("`weapp-vite ide logs` 当前仅支持微信小程序平台。");
if (action === "doctor") {
await runIdeDoctor(resolved.projectPath, options);
return;
}
const projectPath = resolved.projectPath;
if (!projectPath) throw new Error("无法解析微信开发者工具项目目录,请显式传入 root 或检查 project.config.json。");
if (action === "setup") {
const result = await bootstrapWechatDevtoolsSettings({
projectPath,
trustProject: options.trustProject
});
logger_default.info(`已完成微信开发者工具配置预热:扫描实例 ${result.touchedInstanceCount} 个,检测服务端口配置 ${result.detectedSecurityCount} 处,写入项目信任 ${result.trustedProjectCount} 处。`);
return;
}
if (options.open) await openIde(resolved.platform, projectPath, {
openRecovery: options.openRecovery,
trustProject: options.trustProject,
openStrategy: options.ideOpenStrategy
});
if (action === "info") {
const result = await getWechatIdeToolInfo({ projectPath });
logger_default.info(formatIdeOutput(result, options));
return;
}
if (action === "test-accounts") {
const result = await getWechatIdeTestAccounts({ projectPath });
logger_default.info(formatIdeOutput(result, options));
return;
}
if (action === "ticket") {
const result = await getWechatIdeTicket({ projectPath });
logger_default.info(formatIdeOutput(result, options));
return;
}
if (action === "ticket:set") {
if (!options.ticket) throw new Error("`weapp-vite ide ticket:set` 需要提供 --ticket。");
await setWechatIdeTicket({
projectPath,
ticket: options.ticket
});
logger_default.info(`已设置微信开发者工具 ticket:${options.ticket}`);
return;
}
if (action === "ticket:refresh") {
await refreshWechatIdeTicket({ projectPath });
logger_default.info("已刷新微信开发者工具 ticket。");
return;
}
const forwardConsoleOptions = await resolveForwardConsoleOptions({
...resolved.weappViteConfig,
forwardConsole: resolved.weappViteConfig?.forwardConsole === false ? { enabled: true } : {
...typeof resolved.weappViteConfig?.forwardConsole === "object" ? resolved.weappViteConfig.forwardConsole : {},
enabled: true
}
});
const latestHmrSummary = await readLatestHmrProfileSummary({
cwd: resolved.cwd ?? process.cwd(),
relativeCwd: (value) => resolved.cwd ? value.replace(`${resolved.cwd}/`, "") : value,
weappViteConfig: resolved.weappViteConfig
});
if (latestHmrSummary) logger_default.info(latestHmrSummary.line);
const session = await startForwardConsoleBridge({
projectPath,
port: resolveProjectAutomatorPort(projectPath),
agentName: void 0,
logLevels: forwardConsoleOptions.logLevels,
unhandledErrors: forwardConsoleOptions.unhandledErrors,
onReadyMessage: "[forwardConsole] 已进入持续监听模式,按 Ctrl+C 退出。"
});
await waitForTermination(async () => {
logger_default.info("[forwardConsole] 正在关闭日志桥接...");
await session.close();
});
}
/**
* @description 注册 IDE 相关子命令。
*/
function registerIdeCommand(cli) {
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) => {
await runIdeCommand(action, root, options);
});
}
//#endregion
//#region src/cli/commands/init.ts
function registerInitCommand(cli) {
cli.command("init").action(async () => {
try {
await initConfig({ command: "weapp-vite" });
} catch (error) {
logger_default.error(error);
}
});
}
//#endregion
//#region src/cli/mcpClient/shared.ts
const CODEX_BLOCK_PREFIX = "# >>> weapp-vite mcp ";
const CODEX_BLOCK_SUFFIX = " >>>";
const CODEX_BLOCK_END_PREFIX = "# <<< weapp-vite mcp ";
const CODEX_BLOCK_END_SUFFIX = " <<<";
const DEFAULT_HTTP_TIMEOUT_MS = 1500;
const WORKSPACE_FOLDER_TOKEN = "${workspaceFolder}";
const REG_CodexUrl = /^\s*url\s*=\s*"([^"]+)"/m;
const REG_CodexCommand = /^\s*command\s*=\s*"([^"]+)"/m;
const REG_CodexArgs = /^\s*args\s*=\s*\[([^\]]*)\]/m;
const REG_FirstTomlString = /"([^"]+)"/;
function resolveTargetWorkspaceRoot(target) {
if (target.client === "cursor") return path.dirname(path.dirname(target.configPath));
if (target.client === "claude-code") return path.dirname(target.configPath);
return process.cwd();
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function renderTomlString(value) {
return JSON.stringify(value);
}
function sanitizeServerNameSegment(value) {
return value.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
}
function resolveServerName(workspaceRoot) {
return `weapp-vite-${sanitizeServerNameSegment(path.basename(workspaceRoot)) || "project"}`;
}
function resolveNodeBinCommand() {
return process.execPath || "node";
}
function resolveProjectCliBin(workspaceRoot) {
return path.join(workspaceRoot, "node_modules", "weapp-vite", "bin", "weapp-vite.js");
}
function renderCommandArgs(args) {
return args.map(renderTomlString).join(", ");
}
function renderCodexBlock(serverName, entry) {
const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
if (entry.url) return `${startMarker}
[mcp_servers.${serverName}]
url = ${renderTomlString(entry.url)}
${endMarker}
`;
return `${startMarker}
[mcp_servers.${serverName}]
command = ${renderTomlString(entry.command ?? resolveNodeBinCommand())}
args = [${renderCommandArgs(entry.args ?? [])}]
${endMarker}
`;
}
function renderJsonPreview(serverName, entry) {
return `${JSON.stringify({ mcpServers: { [serverName]: entry } }, null, 2)}
`;
}
function resolveCommandEntry(client, workspaceRoot) {
const binPath = resolveProjectCliBin(workspaceRoot);
if (client === "cursor") return {
command: "node",
args: [
`${WORKSPACE_FOLDER_TOKEN}/node_modules/weapp-vite/bin/weapp-vite.js`,
"mcp",
"--workspace-root",
WORKSPACE_FOLDER_TOKEN
]
};
return {
command: resolveNodeBinCommand(),
args: [
binPath,
"mcp",
"--workspace-root",
workspaceRoot
]
};
}
function resolveHttpEntry(client, url) {
if (client === "claude-code") return {
type: "http",
url
};
return { url };
}
function resolveConfigEntry(client, transport, workspaceRoot, url) {
return transport === "http" ? resolveHttpEntry(client, url ?? "") : resolveCommandEntry(client, workspaceRoot);
}
function resolveCodexBlockPattern(serverName) {
const startMarker = `${CODEX_BLOCK_PREFIX}${serverName}${CODEX_BLOCK_SUFFIX}`;
const endMarker = `${CODEX_BLOCK_END_PREFIX}${serverName}${CODEX_BLOCK_END_SUFFIX}`;
return new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
}
function upsertCodexManagedBlock(content, serverName, block) {
const withoutExisting = content.trimEnd().replace(resolveCodexBlockPattern(serverName), "").trimEnd();
if (!withoutExisting) return block;
return `${withoutExisting}\n\n${block}`;
}
function parseJsonConfig(content, configPath) {
if (!content.trim()) return {};
try {
return JSON.parse(content);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`无法解析 MCP 配置文件 ${configPath}:${message}`);
}
}
function resolveTarget(client, workspaceRoot) {
const serverName = resolveServerName(workspaceRoot);
if (client === "codex") return {
client,
configPath: path.join(os.homedir(), ".codex", "config.toml"),
displayName: "Codex",
serverName
};
if (client === "claude-code") return {
client,
configPath: path.join(workspaceRoot, ".mcp.json"),
displayName: "Claude Code",
serverName
};
return {
client,
configPath: path.join(workspaceRoot, ".cursor", "mcp.json"),
displayName: "Cursor",
serverName
};
}
//#endregion
//#region src/cli/mcpClient/inspect.ts
async function probeHttpEndpoint(url) {
const controller = new AbortController();
const timer = setTimeout$1(() => {
controller.abort();
}, DEFAULT_HTTP_TIMEOUT_MS);
try {
return (await fetch(url, {
method: "GET",
signal: controller.signal
})).status > 0;
} catch {
return false;
} finally {
clearTimeout$1(timer);
controller.abort();
}
}
async function inspectJsonConfig(target) {
if (!await fs$1.pathExists(target.configPath)) return {
configExists: false,
configPath: target.configPath,
displayName: target.displayName,
issues: [`未找到配置文件:${target.configPath}`],
serverName: target.serverName
};
const entry = parseJsonConfig(await fs$1.readFile(target.configPath, "utf8"), target.configPath).mcpServers?.[target.serverName];
if (!entry) return {
configExists: true,
configPath: target.configPath,
displayName: target.displayName,
issues: [`配置文件中未找到服务器 ${target.serverName}`],
serverName: target.serverName
};
const transport = entry.url ? "http" : "command";
const issues = [];
let httpReachable;
if (transport === "command") {
const normalizedBin = (entry.args?.[0])?.replaceAll(WORKSPACE_FOLDER_TOKEN, resolveTargetWorkspaceRoot(target));
if (!normalizedBin || !await fs$1.pathExists(normalizedBin)) issues.push(`未找到本地 CLI 入口:${normalizedBin ?? "缺少 args[0]"}`);
} else if (entry.url) {
httpReachable = await probeHttpEndpoint(entry.url);
if (!httpReachable) issues.push(`HTTP MCP 地址不可达:${entry.url}`);
}
return {
configExists: true,
configPath: target.configPath,
displayName: target.displayName,
httpReachable,
issues,
serverName: target.serverName,
transport
};
}
async function inspectCodexConfig(target) {
if (!await fs$1.pathExists(target.configPath)) return {
configExists: false,
configPath: target.configPath,
displayName: target.displayName,
issues: [`未找到配置文件:${target.configPath}`],
serverName: target.serverName
};
const blockMatch = (await fs$1.readFile(target.configPath, "utf8")).match(resolveCodexBlockPattern(target.serverName));
if (!blockMatch?.[0]) return {
configExists: true,
configPath: target.configPath,
displayName: target.displayName,
issues: [`未找到 weapp-vite 生成的 Codex 配置区块:${target.serverName}`],
serverName: target.serverName
};
const block = blockMatch[0];
const issues = [];
const urlMatch = block.match(REG_CodexUrl);
const commandMatch = block.match(REG_CodexCommand);
const argsMatch = block.match(REG_CodexArgs);
if (urlMatch?.[1]) {
const reachable = await probeHttpEndpoint(urlMatch[1]);
if (!reachable) issues.push(`HTTP MCP 地址不可达:${urlMatch[1]}`);
return {
configExists: true,
configPath: target.configPath,
displayName: target.displayName,
httpReachable: reachable,
issues,
serverName: target.serverName,
transport: "http"
};
}
const binPath = (argsMatch?.[1]?.match(REG_FirstTomlString))?.[1];
if (!commandMatch?.[1]) issues.push("Codex 配置区块缺少 command 字段");
if (!binPath || !await fs$1.pathExists(binPath)) issues.push(`未找到本地 CLI 入口:${binPath ?? "缺少 args[0]"}`);
return {
configExists: true,
configPath: target.configPath,
displayName: target.displayName,
issues,
serverName: target.serverName,
transport: "command"
};
}
async function inspectMcpClientConfig(options) {
const target = resolveTarget(options.client, path.resolve(options.workspaceRoot));
if (target.client === "codex") return await inspectCodexConfig(target);
return await inspectJsonConfig(target);
}
//#endregion
//#region src/cli/mcpClient/plan.ts
function resolveSupportedMcpClient(input) {
if (input === "codex" || input === "claude-code" || input === "cursor") return input;
throw new Error(`不支持的 MCP 客户端:${input}。当前仅支持 codex、claude-code、cursor。`);
}
function buildMcpClientConfigPlan(options) {
const workspaceRoot = path.resolve(options.workspaceRoot);
const target = resolveTarget(options.client, workspaceRoot);
const entry = resolveConfigEntry(options.client, options.transport, workspaceRoot, options.url);
if (options.transport === "http" && !entry.url) throw new Error("HTTP 模式缺少 MCP 服务地址,请使用 --url 指定或在当前项目中检测到可用地址。");
return {
entry,
preview: target.client === "codex" ? renderCodexBlock(target.serverName, entry) : renderJsonPreview(target.serverName, entry),
target,
transport: options.transport
};
}
function formatMcpQuickStart(options) {
const suffix = options.transport === "http" && options.httpUrl ? ` --transport http --url ${options.httpUrl}` : "";
return [
"在 AI 工具中接入 weapp-vite MCP:",
` - Codex: wv mcp init codex${suffix}`,
` - Claude Code: wv mcp init claude-code${suffix}`,
` - Cursor: wv mcp init cursor${suffix}`
];
}
//#endregion
//#region src/cli/mcpClient/index.ts
async function writeMcpClientConfig(plan) {
await fs$1.ensureDir(path.dirname(plan.target.configPath));
const existing = await fs$1.readFile(plan.target.configPath, "utf8").catch(() => "");
if (plan.target.client === "codex") {
const nextContent = upsertCodexManagedBlock(existing, plan.target.serverName, plan.preview);
await fs$1.writeFile(plan.target.configPath, nextContent, "utf8");
return;
}
const parsed = parseJsonConfig(existing, plan.target.configPath);
const nextConfig = {
...parsed,
mcpServers: {
...parsed.mcpServers ?? {},
[plan.target.serverName]: plan.entry
}
};
await fs$1.writeFile(plan.target.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
}
//#endregion
//#region src/cli/commands/mcp.ts
function resolvePort(port) {
if (typeof port === "number" && Number.isInteger(port)) return port;
if (typeof port === "string" && port.trim()) {
const parsed = Number.parseInt(port, 10);
if (Number.isInteger(parsed)) return parsed;
}
}
async function resolveHttpUrl(options) {
if (options.url?.trim()) return options.url.trim();
const workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : process.cwd();
const originalCwd = process.cwd();
try {
if (workspaceRoot !== originalCwd) process.chdir(workspaceRoot);
const loaded = await loadConfig(resolveConfigFile(options));
const resolved = resolveWeappMcpConfig(loaded?.config?.weapp?.mcp);
return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
} catch {
const resolved = resolveWeappMcpConfig(void 0);
return `http://${resolved.host}:${resolved.port}${resolved.endpoint}`;
} finally {
if (process.cwd() !== originalCwd) process.chdir(originalCwd);
}
}
async function confirmWrite() {
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
return await runWithSuspendedSharedInput(async () => {
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
try {
const normalized = (await rl.question("是否写入配置文件?(Y/n) ")).trim().toLowerCase();
return normalized === "" || normalized === "y" || normalized === "yes";
} finally {
rl.close();
}
});
}
function resolveClientTransport(transport) {
return transport === "http" ? "http" : "command";
}
async function handlePrint(clientName, options, write = false) {
const client = resolveSupportedMcpClient(clientName);
const workspaceRoot = options.workspaceRoot ?? process.cwd();
const transport = resolveClientTransport(options.transport);
const plan = buildMcpClientConfigPlan({
client,
transport,
url: transport === "http" ? await resolveHttpUrl(options) : void 0,
workspaceRoot
});
logger_default.info(`${plan.target.displayName} 配置文件:${plan.target.configPath}`);
logger_default.info(`服务器名称:${plan.target.serverName}`);
process.stdout.write(`${plan.preview}\n`);
if (!write) return;
if (options.yes || await confirmWrite()) {
await writeMcpClientConfig(plan);
logger_default.success(`已写入 ${plan.target.displayName} MCP 配置。`);
logger_default.info(`请重启 ${plan.target.displayName},然后执行:wv mcp doctor ${client}`);
return;
}
logger_default.info("已取消写入。");
}
async function handleDoctor(clientName, options) {
const result = await inspectMcpClientConfig({
client: resolveSupportedMcpClient(clientName),
workspaceRoot: options.workspaceRoot ?? process.cwd()
});
logger_default.info(`${result.displayName} 配置文件:${result.configPath}`);
logger_default.info(`服务器名称:${result.serverName}`);
if (!result.configExists || result.issues.length > 0) {
for (const issue of result.issues) logger_default.warn(issue);
throw new Error("MCP 客户端配置检查未通过。");
}
if (result.transport) logger_default.info(`传输模式:${result.transport}`);
if (result.httpReachable !== void 0) logger_default.info(`HTTP 服务可达:${result.httpReachable ? "是" : "否"}`);
logger_default.success("MCP 客户端配置检查通过。");
}
async function handleServer(options) {
const resolvedTransport = options.transport === "http" ? "streamable-http" : options.transport === "command" ? void 0 : options.transport;
await startWeappViteMcpServer({
endpoint: options.endpoint,
host: options.host,
port: resolvePort(options.port),
restEndpoint: options.rest === false ? false : options.restEndpoint,
transport: resolvedTransport,
unref: options.unref,
workspaceRoot: options.workspaceRoot
});
for (const line of formatMcpQuickStart({
httpUrl: resolvedTransport === "streamable-http" ? `http://${options.host ?? "127.0.0.1"}:${resolvePort(options.port) ?? 3088}${options.endpoint ?? "/mcp"}` : void 0,
transport: resolvedTransport === "streamable-http" ? "http" : "command"
})) logger_default.info(line);
}
function registerMcpCommand(cli) {
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) => {
const [subcommand, client] = args;
if (subcommand === "init") {
if (!client) throw new Error("缺少客户端名称,请使用:wv mcp init <codex|claude-code|cursor>");
await handlePrint(client, options, true);
return;
}
if (subcommand === "print") {
if (!client) throw new Error("缺少客户端名称,请使用:wv mcp print <codex|claude-code|cursor>");
await handlePrint(client, options, false);
return;
}
if (subcommand === "doctor") {
if (!client) throw new Error("缺少客户端名称,请使用:wv mcp doctor <codex|claude-code|cursor>");
await handleDoctor(client, options);
return;
}
if (subcommand) throw new Error(`未知的 mcp 子命令:${subcommand}`);
await handleServer(options);
});
}
//#endregion
//#region src/cli/commands/npm.ts
function registerNpmCommand(cli) {
cli.command("npm").alias("build:npm").alias("build-npm").action(async () => {
try {
await buildWechatIdeNpm({ projectPath: process.cwd() });
} catch (error) {
logger_default.error(error);
}
});
}
//#endregion
//#region src/cli/mcpDetached.ts
function resolveCliEntrypoint() {
return process.argv[1];
}
function resolveManifestPath(cwd) {
return path$1.join(cwd, ".weapp-vite", "mcp-runtime.json");
}
async function isTcpPortOpen(host, port) {
return await new Promise((resolve) => {
const socket = net.createConnection({
host,
port
});
const cleanup = () => {
socket.removeAllListeners();
socket.destroy();
};
socket.once("connect", () => {
cleanup();
resolve(true);
});
socket.once("error", () => {
cleanup();
resolve(false);
});
socket.setTimeout(500, () => {
cleanup();
resolve(false);
});
});
}
async function writeRuntimeManifest(cwd, manifest) {
const manifestPath = resolveManifestPath(cwd);
await mkdir(path$1.dirname(manifestPath), { recursive: true });
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
}
async function maybeStartDetachedMcpServer(options) {
const resolvedMcp = resolveWeappMcpConfig(options.mcpConfig, {
agentName: options.agentName,
cwd: options.cwd,
isAgent: options.isAgent
});
if (!resolvedMcp.enabled || !resolvedMcp.autoStart) return;
const entrypoint = resolveCliEntrypoint();
if (!entrypoint) {
logger_default.warn("[mcp] 找不到当前 CLI 入口,跳过 MCP 后台自动启动。");
return;
}
const url = `http://${resolvedMcp.host}:${resolvedMcp.port}${resolvedMcp.endpoint}`;
const restUrl = resolvedMcp.restEndpoint === false ? void 0 : `http://${resolvedMcp.host}:${resolvedMcp.port}${resolvedMcp.restEndpoint}`;
if (await isTcpPortOpen(resolvedMcp.host, resolvedMcp.port)) {
logger_default.info(`[mcp] MCP 服务已存在,继续复用:${colors.cyan(url)}`);
return;
}
const args = [
entrypoint,
"mcp",
"--transport",
"streamable-http",
"--host",
resolvedMcp.host,
"--port",
String(resolvedMcp.port),
"--endpoint",
resolvedMcp.endpoint,
"--workspace-root",
options.cwd
];
if (resolvedMcp.restEndpoint === false) args.push("--no-rest");
else args.push("--rest-endpoint", resolvedMcp.restEndpoint);
const child = spawn(process.execPath, args, {
cwd: options.cwd,
detached: true,
stdio: "ignore"
});
child.unref();
if (child.pid) await writeRuntimeManifest(options.cwd, {
agentName: resolvedMcp.agentName,
endpoint: resolvedMcp.endpoint,
pid: child.pid,
projectRoot: options.cwd,
restUrl,
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
startedBy: "weapp-vite open",
transport: "streamable-http",
url
});
const suffix = resolvedMcp.agentName ? `(AI 终端:${resolvedMcp.agentName})` : "";
logger_default.success(`MCP 服务已在后台自动启动:${suffix}`);
logger_default.info(` ➜ ${colors.cyan(url)}`);
if (restUrl) logger_default.info(` REST ➜ ${colors.cyan(restUrl)}`);
for (const line of formatMcpQuickStart({
httpUrl: url,
transport: "http"
})) logger_default.info(line);
}
//#endregion
//#region src/cli/mcpOptions.ts
function applyMcpCliOptions(config, options) {
if (options.mcp === false) return false;
if (options.mcp === true) return {
...typeof config === "object" && config ? config : {},
enabled: true,
autoStart: true
};
return config;
}
//#endregion
//#region src/cli/commands/open.ts
function registerOpenCommand(cli) {
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) => {
filterDuplicateOptions(options);
const configFile = resolveConfigFile(options);
const targets = resolveRuntimeTargets(options);
const ideBackend = getBackendForCapability(targets, "miniprogram", "ide");
if (!ideBackend) throw new Error("`weapp-vite open` 当前仅支持小程序平台。");
const { cwd, platform, projectPath, mpDistRoot, weappViteConfig } = await resolveIdeCommandContext({
configFile,
mode: options.mode ?? "development",
platform: ideBackend.platform,
projectPath: root,
cliPlatform: targets.rawPlatform
});
const latestHmrSummary = await readLatestHmrProfileSummary({
cwd: cwd ?? process.cwd(),
relativeCwd: (value) => cwd ? value.replace(`${cwd}/`, "") : value,
weappViteConfig
});
if (latestHmrSummary) logger_default.info(latestHmrSummary.line);
const cwdForMcp = cwd ?? process.cwd();
const aiEnvironment = await detectAiDevelopmentEnvironment();
await maybeStartDetachedMcpServer({
agentName: aiEnvironment.agentName,
cwd: cwdForMcp,
isAgent: aiEnvironment.isAgent,
mcpConfig: applyMcpCliOptions(weappViteConfig?.mcp, options)
});
await openIde(platform, projectPath ?? resolveIdeProjectRoot(mpDistRoot, process.cwd()), {
loginRetry: options.loginRetry,
loginRetryTimeout: options.loginRetryTimeout,
nonInteractive: options.nonInteractive,
openRecovery: options.openRecovery,
trustProject: options.trustProject,
openStrategy: options.ideOpenStrategy
});
});
}
//#endregion
//#region src/cli/commands/prepare.ts
function resolvePrepareRoot(input) {
const values = Array.isArray(input) ? input.filter((item) => typeof item === "string" && item.length > 0) : [];
if (values.length === 0) return ".";
return (values[0] === "prepare" ? values.slice(1) : values)[0] ?? ".";
}
function formatPrepareSkipMessage$1(error) {
return `跳过 .weapp-vite 支持文件预生成:${error instanceof Error ? error.message : String(error)}`;
}
function resolvePreparePlatform(options) {
return typeof options.platform === "string" ? options.platform : typeof options.p === "string" ? options.p : void 0;
}
function registerPrepareCommand(cli) {
cli.command("prepare [...input]", "generate .weapp-vite support files").option("-p, --platform <platform>", `[string] target platform (weapp | web)`).action(async (input, options) => {
try {
filterDuplicateOptions(options);
const cwd = path.resolve(resolvePrepareRoot(input));
const cliPlatform = resolvePreparePlatform(options);
const ctx = await createCompilerContext({
cwd,
isDev: false,
mode: typeof options.mode === "string" ? options.mode : "development",
configFile: resolveConfigFile(options),
configLoader: "native",
syncSupportFiles: false,
preloadAppEntry: false,
...cliPlatform ? { cliPlatform } : {}
});
const supportFiles = await syncProjectSupportFiles(ctx);
for (const warning of supportFiles.managedTsconfigWarnings) logger_default.warn(warning);
logger_default.info("已生成 .weapp-vite 支持文件。");
} catch (error) {
logger_default.warn(`[prepare] ${formatPrepareSkipMessage$1(error)}`);
}
});
}
//#endregion
//#region src/cli/devHotkeys/devtools.ts
async function appendLatestHmrSummary(baseSummary, options) {
const summary = await readLatestHmrProfileSummary({
cwd: options.cwd,
relativeCwd: (value) => value.replace(`${options.cwd}/`, ""),
weappViteConfig: options.weappViteConfig
});
if (!summary) return baseSummary;
return `${baseSummary};${summary.line}`;
}
/**
* @description 重置当前 DevTools automator 共享会话。
*/
async function runResetDevtoolsSessionAction(options) {
logger_default.info("[dev action] 正在重置当前 DevTools 会话...");
await closeSharedMiniProgram(options.projectPath);
logger_default.success("[dev action] 当前 DevTools 会话已重置。");
return "已重置当前 DevTools 会话";
}
/**
* @description 重置共享会话并重开当前项目,以恢复 DevTools 状态。
*/
async function runResetAndReopenDevtoolsAction(options) {
if (!options.openIde) {
logger_default.warn("[dev action] 当前 dev 会话未提供重新打开微信开发者工具的能力。");
return "重置并重开微信开发者工具不可用";
}
logger_default.info("[dev action] 正在重置当前 DevTools 会话并重开项目...");
await closeSharedMiniProgram(options.projectPath);
const summary = await options.openIde();
logger_default.success("[dev action] 当前 DevTools 会话已重置,并已重新打开项目。");
return await appendLatestHmrSummary(summary ?? "已重置当前 DevTools 会话并重新打开项目", options);
}
/**
* @description 手动触发一次当前小程序 dev 重新构建。
*/
async function runDevRebuildAction(options) {
if (!options.rebuild) {
logger_default.warn("[dev action] 当前 dev 会话未提供手动重新构建能力。");
return "手动重新构建不可用";
}
logger_default.info("[dev action] 正在手动重新构建当前小程序产物...");
const summary = await options.rebuild();
logger_default.success("[dev action] 当前小程序产物已手动重新构建。");
return summary ?? "已手动重新构建当前小程序产物";
}
/**
* @description 重新打开当前微信开发者工具项目,并主动清理共享 automator 会话。
*/
async function runOpenIdeAction(options) {
if (!options.openIde) {
logger_default.warn("[dev action] 当前 dev 会话未提供重新打开微信开发者工具的能力。");
return "重新打开微信开发者工具不可用";
}
logger_default.info(`[dev action] 正在重新打开微信开发者工具项目:${colors.cyan(options.projectPath)}`);
await closeSharedMiniProgram(options.projectPath);
const summary = await options.openIde();
logger_default.success("[dev action] 微信开发者工具项目已重新打开。");
return await appendLatestHmrSummary(summary ?? "已重新打开微信开发者工具项目", options);
}
//#endregion
//#region src/cli/devHotkeys/screenshot.ts
const DEV_SCREENSHOT_DIR = ".weapp-vite/dev-screenshots";
const DEFAULT_SCREENSHOT_TIMEOUT = 12e4;
/**
* @description 生成开发态截图输出路径。
*/
function resolveDevScreenshotOutputPath(cwd, now = /* @__PURE__ */ new Date()) {
const stamp = now.toISOString().replace(/[:.]/g, "-");
return path.join(cwd, DEV_SCREENSHOT_DIR, `screenshot-${stamp}.png`);
}
function formatLogPath(cwd, targetPath) {
const relativePath = path.relative(cwd, targetPath);
if (!relativePath || relativePath.startsWith("..")) return targetPath;
return relativePath;
}
function formatResolvedScreenshotPath(cwd, fallbackPath, result) {
return formatLogPath(cwd, result.path ?? fallbackPath);
}
async function resolveDefaultScreenshotPage(cwd) {
try {
const raw = await fs$2.readFile(path.join(cwd, "app.json"), "utf8");
const appJson = JSON.parse(raw);
const [firstPage] = Array.isArray(appJson.pages) ? appJson.pages : [];
return typeof firstPage === "string" && firstPage.trim() ? firstPage : void 0;
} catch {
return;
}
}
/**
* @description 执行当前页面截图并输出结果日志。
*/
async function runScreenshotAction(options) {
const outputPath = resolveDevScreenshotOutputPath(options.cwd);
await fs$2.mkdir(path.dirname(outputPath), { recursive: true });
const page = await resolveDefaultScreenshotPage(options.cwd);
logger_default.info(`[dev action] 正在截图当前页面,输出到 ${colors.cyan(formatLogPath(options.cwd, outputPath))}`);
const resumeForwardConsole = await pauseActiveForwardConsole();
try {
const result = await takeScreenshot({
projectPath: options.projectPath,
fullPage: true,
...page ? { page } : {},
preserveProjectRoot: true,
port: resolveProjectAutomatorPort(options.projectPath),
retryWithFreshSession: false,
outputPath,
timeout: DEFAULT_SCREENSHOT_TIMEOUT
});
const resolvedPath = formatResolvedScreenshotPath(options.cwd, outputPath, result);
logger_default.success(`[dev action] 当前页面截图完成:${colors.cyan(resolvedPath)}`);
return resolvedPath;
} finally {
await resumeForwardConsole?.();
}
}
//#endregion
//#region src/cli/devHotkeys/actions.ts
const DEV_HOTKEY_GROUPS = [
{
key: "development",
title: "开发动作"
},
{
key: "devtools",
title: "会话动作"
},
{
key: "process",
title: "进程控制"
},
{
key: "help",
title: "帮助"
}
];
const DEV_HOTKEY_DEFINITIONS = [
{
description: "截图当前页面并保存到本地",
group: "development",
key: "s",
label: "截图",
pendingLabel: "正在截图当前页面",
run: async ({ options }) => {
return `截图已保存到 ${await runScreenshotAction(options)}`;
}
},
{
description: "手动重新构建当前小程序产物",
group: "development",
key: "r",
label: "手动重新构建产物",
pendingLabel: "正在手动重新构建当前小程序产物",
run: async ({ options }) => {
return await runDevRebuildAction(options);
}
},
{
description: "重置当前 DevTools automator 会话",
group: "devtools",
key: "c",
label: "重置 DevTools 会话",
pendingLabel: "正在重置当前 DevTools 会话",
run: async ({ options }) => {
return await runResetDevtoolsSessionAction(options);
}
},
{
description: "重置会话并重开当前微信开发者工具项目",
group: "devtools",
key: "C",
label: "重置并重开项目",
pendingLabel: "正在重置当前 DevTools 会话并重开项目",
run: async ({ options }) => {
return await runResetAndReopenDevtoolsAction(options);
}
},
{
description: "重新打开当前微信开发者工具项目",
group: "devtools",
key: "o",
label: "重新打开微信开发者工具",
pendingLabel: "正在重新打开微信开发者工具项目",
run: async ({ options }) => {
return await runOpenIdeAction(options);
}
},
{
description: "开关 MCP 服务",
group: "devtools",
key: "m",
label: "MCP 切换",
run: async ({ toggleMcp }) => {
return await toggleMcp();
}
},
{
description: "退出当前 dev",
group: "process",
key: "q"
},
{
description: "强制中断当前 dev",
group: "process",
key: "Ctrl+C"
},
{
description: "暂时挂起当前 dev,恢复终端控制",
group: "process",
key: "Ctrl+Z"
},
{
description: "重新显示这份帮助",
group: "help",
key: "h"
}
];
function resolveDevHotkeyRowsByGroup() {
return DEV_HOTKEY_GROUPS.map((group) => ({
key: group.key,
rows: DEV_HOTKEY_DEFINITIONS.filter((action) => action.group === group.key),
title: group.title
}));
}
function resolveRunnableHotkeyDefinition(input) {
return DEV_HOTKEY_DEFINITIONS.find((action) => action.run && action.key === input);
}
//#endregion
//#region package.json
var version = "7.1.2";
//#endregion
//#region src/cli/devHotkeys/format.ts
const FULLWIDTH_ASCII_START = 65281;
const FULLWIDTH_ASCII_END = 65374;
const FULLWIDTH_ASCII_OFFSET = 65248;
const REG_PENDING_PREFIX = /^正在/;
function formatProjectLabel(cwd) {
return path.basename(cwd) || cwd;
}
function formatMcpStatus(state) {
if (!state.mcpEnabled) return "已禁用";
return state.mcpRunning ? "运行中" : "未启动";
}
function formatMcpHelpRows(state) {
if (!state.mcpEnabled || !state.mcpHttpUrl) return [];
return [
"",
"MCP 接入",
`HTTP:${colors.cyan(state.mcpHttpUrl)}`,
...state.mcpRestUrl ? [`REST:${colors.cyan(state.mcpRestUrl)}`] : [],
...formatMcpQuickStart({
httpUrl: state.mcpHttpUrl,
transport: "http"
})
];
}
function formatFooterLine(state) {
if (state.currentAction) return `执行中 ${state.currentAction}`;
return "就绪 等待操作...";
}
function formatHotkeyRows(rows) {
const key = (value) => colors.bold(colors.green(value));
const formattedRows = rows.map((row) => ({
description: row.description,
key: key(row.key)
}));
const keyColumnWidth = Math.max(...formattedRows.map((row) => row.key.length));
return formattedRows.map(({ key, description }) => `按 ${key.padEnd(keyColumnWidth)} ${description}`);
}
function formatExtraHotkeyRows(rows) {
const key = (value) => colors.bold(colors.green(value));
const formattedRows = rows.map(([hotkey, description]) => ({
description,
key: key(hotkey)
}));
const keyColumnWidth = Math.max(...formattedRows.map((row) => row.key.length));
return formattedRows.map(({ key, description }) => `按 ${key.padEnd(keyColumnWidth)} ${description}`);
}
/**
* @description 生成带状态的开发态快捷键帮助文本。
*/
function formatDevHotkeyHelpWithState(state) {
const sections = resolveDevHotkeyRowsByGroup().filter((section) => section.rows.length > 0).flatMap((section) => [
"",
section.title,
...formatHotkeyRows(section.rows)
]);
const retrySections = [
"",
"登录重试",
...formatExtraHotkeyRows([[RETRY_CONFIRM_KEYS.join(" / "), "微信开发者工具登录失效后确认重试当前命令"], [RETRY_CANCEL_KEYS.join(" / "), "取消当前登录重试提示"]])
];
return [
`${colors.bold(colors.green("DEV"))} weapp-vite v${version} ${state.projectLabel ?? "weapp"}`,
...sections,
...retrySections,
"",
`当前状态:${state.currentAction ?? "等待操作"} / MCP ${formatMcpStatus(state)}`,
...state.lastAction ? [`最近操作:${state.lastAction}`] : [],
...formatMcpHelpRows(state)
].join("\n");
}
/**
* @description 生成带状态的开发态快捷键简短提示。
*/
function formatDevHotkeyHintWithState(state) {
const key = (value) => colors.bold(colors.green(value));
if (state.currentAction) return `${formatFooterLine(state)},按 ${key("h")} 显示帮助,按 ${key("q")} 退出`;
if (state.lastAction) return `开发快捷键已就绪,最近操作:${state.lastAction},按 ${key("h")} 显示帮助,按 ${key("q")} 退出`;
return `开发快捷键已就绪,按 ${key("h")} 显示帮助,按 ${key("q")} 退出`;
}
function normalizeInputChar(input) {
if (input.length !== 1) return input;
const codePoint = input.codePointAt(0);
if (!codePoint) return input;
if (codePoint >= FULLWIDTH_ASCII_START && codePoint <= FULLWIDTH_ASCII_END) return String.fromCodePoint(codePoint - FULLWIDTH_ASCII_OFFSET);
return input;
}
function resolveProjectLabel(cwd) {
return formatProjectLabel(cwd);
}
//#endregion
//#region src/cli/devHotkeys/mcp.ts
const REG_EADDRINUSE$1 = /EADDRINUSE/;
function formatMcpUrl$1(host, port, endpoint) {
return `http://${host}:${port}${endpoint}`;
}
function createToggleMcpAction(options) {
const { cwd, getHandle, resolvedMcp, setHandle } = options;
return async function toggleMcp(actionOptions = {}) {
const silent = actionOptions.silent === true;
if (!resolvedMcp.enabled) {
logger_default.warn("[dev action] MCP 已在配置中禁用,跳过切换。");
return "MCP 已禁用";
}
const existingHandle = getHandle();
if (existingHandle?.close) {
const url = formatMcpUrl$1(resolvedMcp.host, resolvedMcp.port, resolvedMcp.endpoint);
logger_default.info(`[dev action] 正在关闭 MCP 服务:${colors.cyan(url)}`);
await existingHandle.close();
setHandle(void 0);
logger_default.success(`[dev action] MCP 服务已关闭:${colors.cyan(url)}`);
return `MCP 已关闭 (${url})`;
}
const url = formatMcpUrl$1(resolvedMcp.host, resolvedMcp.port, resolvedMcp.endpoint);
if (!silent) logger_default.info(`[dev action] 正在启动 MCP 服务:${colors.cyan(url)}`);
let handle;
try {
handle = await startWeappViteMcpServer({
endpoint: resolvedMcp.endpoint,
host: resolvedMcp.host,
port: resolvedMcp.port,
restEndpoint: resolvedMcp.restEndpoint,
transport: "streamable-http",
unref: false,
onReady: () => {},
workspaceRoot: cwd
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (REG_EADDRINUSE$1.test(message)) {
if (!silent) logger_default.info(`[dev action] MCP 服务已存在,继续复用:${colors.cyan(url)}`);
return `MCP 已复用 (${url})`;
}
throw error;
}
setHandle(handle);
const suffix = resolvedMcp.agentName ? `(AI 终端:${resolvedMcp.agentName})` : "";
if (!silent) logger_default.success(`[dev action] MCP 服务已启动:${colors.cyan(url)}${suffix}`);
return `MCP 已启动 (${url})`;
};
}
//#endregion
//#region src/cli/devHotkeys/index.ts
function forwardSigint() {
process.kill(process.pid, "SIGINT");
}
function forwardSigtstp() {
process.kill(process.pid, "SIGTSTP");
}
function formatMcpUrl(host, port, endpoint) {
return `http://${host}:${port}${endpoint}`;
}
/**
* @description 在开发态启动终端快捷键,并将动作输出到本地日志。
*/
function startDevHotkeys(options) {
if (options.platform !== "weapp" || !process.stdin.isTTY) return;
let closed = false;
let running = false;
let mcpHandle;
let onData;
let onKeypress;
let onSigcont;
let currentAction;
let actionStartedAt;
let lastAction;
let lastRenderedPanel = "";
const recentInputs = /* @__PURE__ */ new Map();
const resolvedMcp = resolveWeappMcpConfig(options.mcpConfig, {
agentName: options.agentName,
cwd: options.cwd,
isAgent: options.isAgent
});
const mcpHttpUrl = formatMcpUrl(resolvedMcp.host, resolvedMcp.port, resolvedMcp.endpoint);
const mcpRestUrl = resolvedMcp.restEndpoint === false ? void 0 : formatMcpUrl(resolvedMcp.host, resolvedMcp.port, resolvedMcp.restEndpoint);
const getState = () => ({
currentAction,
lastAction,
mcpEnabled: resolvedMcp.enabled,
mcpHttpUrl,
mcpRunning: Boolean(mcpHandle?.close),
mcpRestUrl,
projectLabel: resolveProjectLabel(options.cwd)
});
let inputSession = createSharedInputSession({
onData: (chunk) => {
onData?.(chunk);
},
onKeypress: (str, key) => {
onKeypress?.(str, key);
}
});
const close = () => {
if (closed) return;
closed = true;
if (onSigcont) process.off("SIGCONT", onSigcont);
inputSession?.close();
inputSession = void 0;
if (mcpHandle?.close) mcpHandle.close().catch((error) => {
logger_default.warn(`[dev action] MCP 服务关闭失败:${error instanceof Error ? error.message : String(error)}`);
});
closeSharedMiniProgram(options.projectPath).catch((error) => {
logger_default.warn(`[dev action] DevTools 会话关闭失败:${error instanceof Error ? error.message : String(error)}`);
});
};
const printPanel = (message, force = false) => {
if (!force && message === lastRenderedPanel) return;
lastRenderedPanel = message;
logger_default.info(message);
};
const printHelp = () => {
printPanel(formatDevHotkeyHelpWithState(getState()), true);
};
const printHint = () => {
printPanel(formatDevHotkeyHintWithState(getState()));
};
const restore = () => {
if (closed) return;
inputSession?.resume();
printHint();
};
const suspendInput = () => {
if (closed) return;
inputSession?.suspend();
};
const suspend = () => {
lastRenderedPanel = "";
suspendInput();
forwardSigtstp();
};
const toggleMcp = createToggleMcpAction({
cwd: options.cwd,
getHandle: () => mcpHandle,
resolvedMcp,
setHandle: (handle) => {
mcpHandle = handle;
}
});
const resolvePendingLabel = (input) => {
if (input === "m") return mcpHandle?.close ? "正在关闭 MCP 服务" : "正在启动 MCP 服务";
};
const runAction = (label, pendingLabel, action) => {
if (running) {
const current = currentAction ?? "已有命令";
const elapsed = actionStartedAt === void 0 ? "" : `(已运行 ${Math.max(0, Date.now() - actionStartedAt)} ms)`;
logger_default.warn(`[dev action] 当前正在${current.replace(REG_PENDING_PREFIX, "")}${elapsed},已忽略重复请求。`);
return;
}
running = true;
actionStartedAt = Date.now();
currentAction = pendingLabel;
printHint();
action().then((summary) => {
if (summary) lastAction = summary;
}).catch((error) => {
logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
}).finally(() => {
running = false;
actionStartedAt = void 0;
currentAction = void 0;
if (!closed) printHint();
});
};
const runBackgroundAction = (label, pendingLabel, action) => {
if (running) return;
running = true;
actionStartedAt = Date.now();
currentAction = pendingLabel;
action().then((summary) => {
if (summary) lastAction = summary;
}).catch((error) => {
logger_default.error(`[dev action] ${label}失败:${error instanceof Error ? error.message : String(error)}`);
}).finally(() => {
running = false;
actionStartedAt = void 0;
currentAction = void 0;
});
};
const handleInput = (input) => {
if (closed) return;
const normalizedInput = normalizeInputChar(input);
if (normalizedInput === "") {
close();
forwardSigint();
return;
}
if (normalizedInput === "") {
suspend();
return;
}
const normalized = normalizedInput.toLowerCase();
if (normalized === "q") {
close();
forwardSigint();
return;
}
if (normalized === "h") {
printHelp();
return;
}
const action = resolveRunnableHotkeyDefinition(normalizedInput) ?? resolveRunnableHotkeyDefinition(normalized);
const run = action?.run;
if (run) runAction(action.label ?? action.description, resolvePendingLabel(normalized) ?? action.pendingLabel ?? `正在执行 ${action.description}`, async () => {
return await run({
options,
toggleMcp
});
});
};
const handleInputOnce = (input, source) => {
const normalizedInput = normalizeInputChar(input);
const now = Date.now();
for (const [token, timestamp] of recentInputs) if (now - Number(timestamp.split(":")[1] ?? 0) > 32) recentInputs.delete(token);
const dedupKey = normalizedInput;
const recentEntry = recentInputs.get(dedupKey);
if (recentEntry !== void 0) {
const [recentSource, recentTimestamp] = recentEntry.split(":");
if (recentSource !== source && now - Number(recentTimestamp) <= 32) return;
}
recentInputs.set(dedupKey, `${source}:${now}`);
handleInput(normalizedInput);
};
onData = (chunk) => {
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
for (const char of text) handleInputOnce(char, "data");
};
onKeypress = (str, key) => {
if (key?.ctrl && key.name === "c") {
handleInputOnce("", "keypress");
return;
}
if (key?.ctrl && key.name === "z") {
handleInputOnce("", "keypress");
return;
}
if (typeof str === "string" && str) handleInputOnce(str, "keypress");
};
onSigcont = () => {
restore();
};
process.on("SIGCONT", onSigcont);
if (!options.silentStartupHint) printHint();
if (resolvedMcp.enabled && resolvedMcp.autoStart) runBackgroundAction("MCP 自动启动", "正在启动 MCP 服务", async () => {
await toggleMcp({ silent: true });
});
return {
close,
restore,
suspend: suspendInput
};
}
//#endregion
//#region src/cli/commands/serve/analyze.ts
const REG_DIST_PAGE_ENTRY = /pages\/.+\/index\.js$/;
const REG_DIST_POSIX_SEP = /\\/g;
function emitDashboardEvents(handle, events) {
handle?.emitRuntimeEvents(events);
}
function formatHmrProfileFile(profile) {
return profile.sourceRootFile ?? profile.relativeFile ?? profile.file ?? "未知文件";
}
function createHmrProfileEvent(profile) {
if (!profile || typeof profile.totalMs !== "number") return;
const file = formatHmrProfileFile(profile);
const counts = [
typeof profile.dirtyCount === "number" ? `dirty ${profile.dirtyCount}` : void 0,
typeof profile.pendingCount === "number" ? `pending ${profile.pendingCount}` : void 0,
typeof profile.emittedCount === "number" ? `emitted ${profile.emittedCount}` : void 0
].filter(Boolean).join(" / ");
return {
kind: "hmr",
level: profile.totalMs >= 1e3 ? "warning" : "success",
title: "mini hmr rebuild completed",
detail: counts ? `${file} 已完成热更新重建(${counts})。` : `${file} 已完成热更新重建。`,
durationMs: profile.totalMs,
tags: ["hmr", "rebuild"],
profile
};
}
function hasAnalyzeData(result) {
return result.packages.length > 0 || result.modules.length > 0;
}
async function collectOutputFiles(root) {
const entries = await fs$2.readdir(root, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const absolutePath = path.join(root, entry.name);
if (entry.isDirectory()) {
files.push(...await collectOutputFiles(absolutePath));
continue;
}
if (entry.isFile()) files.push(absolutePath);
}
return files;
}
function getCompressedSizes(content) {
return {
gzipSize: gzipSync(content).byteLength,
brotliSize: brotliCompressSync(content).byteLength
};
}
async function analyzeUiFallback(ctx) {
const { configService, scanService } = ctx;
await scanService.loadAppEntry();
const subPackageMetas = scanService.loadSubPackages();
const distRoot = configService.outDir;
const absoluteFiles = await collectOutputFiles(distRoot);
const packages = /* @__PURE__ */ new Map();
const ensurePackage = (packageId, packageType) => {
const existing = packages.get(packageId);
if (existing) return existing;
const created = {
id: packageId,
label: packageId === "__main__" ? "主包" : packageType === "independent" ? `独立分包 ${packageId}` : `分包 ${packageId}`,
type: packageType,
files: []
};
packages.set(packageId, created);
return created;
};
const classifyPackage = (relativeFile) => {
const normalized = relativeFile.replace(REG_DIST_POSIX_SEP, "/");
for (const meta of subPackageMetas) {
const root = meta.subPackage.root;
if (root && normalized.startsWith(`${root}/`)) return {
id: root,
type: meta.subPackage.independent ? "independent" : "subPackage"
};
}
return {
id: "__main__",
type: "main"
};
};
for (const absoluteFile of absoluteFiles) {
const relativeFile = path.relative(distRoot, absoluteFile).replace(REG_DIST_POSIX_SEP, "/");
const packageInfo = classifyPackage(relativeFile);
const stat = await fs$2.stat(absoluteFile);
const content = await fs$2.readFile(absoluteFile);
ensurePackage(packageInfo.id, packageInfo.type).files.push({
file: relativeFile,
type: relativeFile.endsWith(".js") ? "chunk" : "asset",
from: packageInfo.type === "independent" ? "independent" : "main",
size: stat.size,
...getCompressedSizes(content),
isEntry: relativeFile === "app.js" || REG_DIST_PAGE_ENTRY.test(relativeFile),
source: relativeFile.endsWith(".js") ? void 0 : relativeFile
});
}
return {
metadata: createAnalyzeMetadata(configService),
packages: Array.from(packages.values()).sort((a, b) => {
if (a.id === "__main__") return -1;
if (b.id === "__main__") return 1;
return a.id.localeCompare(b.id);
}),
modules: [],
subPackages: subPackageMetas.map((meta) => ({
root: meta.subPackage.root ?? "",
independent: Boolean(meta.subPackage.independent),
name: meta.subPackage.name
})).filter((item) => item.root).sort((a, b) => a.root.localeCompare(b.root)),
glassEasel: createGlassEaselAnalyzeResult(ctx)
};
}
function createAnalyzeController(options) {
const { configFile, ctx, options: cliOptions, targets } = options;
const { configService } = ctx;
let analyzeRunId = 0;
let analyzeHandle;
const runAnalyze = async () => {
const startedAt = Date.now();
const previousResult = await readLatestAnalyzeHistorySnapshot(configService);
try {
const result = await analyzeSubpackages(await createCompilerContext({
key: `serve-ui-analyze:${process.pid}:${++analyzeRunId}`,
cwd: configService.cwd,
mode: configService.mode,
isDev: false,
configFile,
inlineConfig: createInlineConfig(targets),
cliPlatform: targets.rawPlatform,
projectConfigPath: cliOptions.projectConfig,
syncSupportFiles: false
}));
if (hasAnalyzeData(result)) {
await writeAnalyzeHistorySnapshot(result, configService);
return {
result,
previousResult,
durationMs: Date.now() - startedAt,
mode: "full"
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[ui] 完整分析失败,已回退到 dist 文件扫描:${message}`);
const result = await analyzeUiFallback(ctx);
await writeAnalyzeHistorySnapshot(result, configService);
return {
result,
previousResult,
durationMs: Date.now() - startedAt,
mode: "fallback",
fallbackReason: message
};
}
const result = await analyzeUiFallback(ctx);
await writeAnalyzeHistorySnapshot(result, configService);
return {
result,
previousResult,
durationMs: Date.now() - startedAt,
mode: "fallback",
fallbackReason: "完整分析结果为空,已回退到 dist 文件扫描。"
};
};
const triggerAnalyzeUpdate = async (reason = "watch") => {
if (!analyzeHandle) return;
emitDashboardEvents(analyzeHandle, [{
kind: reason === "watch" ? "hmr" : "build",
level: "info",
title: reason === "watch" ? "analyze refresh started" : "initial analyze started",
detail: reason === "watch" ? "检测到新的构建结束事件,开始刷新 analyze 面板。" : "开发态 UI 已启动,开始生成第一份 analyze 结果。",
tags: reason === "watch" ? ["watch", "analyze"] : ["initial", "analyze"]
}]);
const next = await runAnalyze();
if (next.mode === "fallback") emitDashboardEvents(analyzeHandle, [{
kind: "diagnostic",
level: "warning",
title: "analyze fallback enabled",
detail: next.fallbackReason ?? "完整分析不可用,已回退到 dist 文件扫描。",
durationMs: next.durationMs,
tags: ["analyze", "fallback"]
}]);
await analyzeHandle.update(next.result, next.previousResult);
emitDashboardEvents(analyzeHandle, [{
kind: next.mode === "fallback" ? "diagnostic" : "build",
level: next.mode === "fallback" ? "warning" : "success",
title: reason === "watch" ? "analyze refresh completed" : "initial analyze completed",
detail: next.mode === "fallback" ? `analyze 已回退到 dist 扫描,当前包含 ${next.result.packages.length} 个包。` : `analyze 已刷新完成,当前包含 ${next.result.packages.length} 个包与 ${next.result.modules.length} 个模块。`,
durationMs: next.durationMs,
tags: next.mode === "fallback" ? ["analyze", "fallback"] : ["analyze", reason === "watch" ? "refresh" : "initial"]
}]);
};
const bindWatcher = (buildResult) => {
let updating = false;
if (analyzeHandle && buildResult && typeof buildResult.on === "function") buildResult.on("event", (event) => {
if (event.code !== "END" || updating) return;
const recentProfiles = ctx.runtimeState.build.hmr.recentProfiles;
const hmrEvent = createHmrProfileEvent(recentProfiles[recentProfiles.length - 1]);
if (hmrEvent) emitDashboardEvents(analyzeHandle, [hmrEvent]);
updating = true;
triggerAnalyzeUpdate("watch").finally(() => {
updating = false;
});
});
return { async runInitialUpdate() {
if (analyzeHandle) {
updating = true;
await triggerAnalyzeUpdate("initial");
updating = false;
}
} };
};
return {
getHandle: () => analyzeHandle,
async startDashboard(startDashboard) {
const initialAnalyze = await runAnalyze();
analyzeHandle = await startDashboard(initialAnalyze.result, {
watch: true,
artifactRoot: configService.outDir,
cwd: configService.cwd,
packageManagerAgent: configService.packageManager.agent,
silentStartupLog: true,
previousResult: initialAnalyze.previousResult,
initialEvents: [{
kind: initialAnalyze.mode === "fallback" ? "diagnostic" : "build",
level: initialAnalyze.mode === "fallback" ? "warning" : "success",
title: initialAnalyze.mode === "fallback" ? "initial analyze fallback completed" : "initial analyze completed",
detail: initialAnalyze.mode === "fallback" ? initialAnalyze.fallbackReason ?? "完整分析不可用,已回退到 dist 文件扫描。" : `开发态首份 analyze 结果已生成,包含 ${initialAnalyze.result.packages.length} 个包与 ${initialAnalyze.result.modules.length} 个模块。`,
durationMs: initialAnalyze.durationMs,
tags: initialAnalyze.mode === "fallback" ? [
"analyze",
"fallback",
"initial"
] : ["analyze", "initial"]
}]
}) ?? void 0;
emitDashboardEvents(analyzeHandle, [{
kind: "command",
level: "success",
title: "dev ui session ready",
detail: `开发态分析面板已启动,当前包含 ${initialAnalyze.result.packages.length} 个包。`,
durationMs: initialAnalyze.durationMs,
tags: ["dev", "ui"]
}]);
if (initialAnalyze.mode === "fallback") emitDashboardEvents(analyzeHandle, [{
kind: "diagnostic",
level: "warning",
title: "initial analyze fallback enabled",
detail: initialAnalyze.fallbackReason ?? "完整分析不可用,已回退到 dist 文件扫描。",
durationMs: initialAnalyze.durationMs,
tags: [
"analyze",
"fallback",
"initial"
]
}]);
},
bindWatcher,
emitRuntimeEvents(events) {
emitDashboardEvents(analyzeHandle, events);
},
waitForExit() {
return analyzeHandle?.waitForExit();
}
};
}
//#endregion
//#region src/cli/commands/serve/shared.ts
function startBackgroundForwardConsole(startForwardConsole, options) {
if (!startForwardConsole) return;
try {
startForwardConsole(options).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[forwardConsole] 后台启动失败:${message}`);
}
}
function resolveServeIdeOpenStrategy(options = {}, fallback = "cli") {
return options.openStrategy ?? (options.useAutomatorOpen ? "automator" : fallback);
}
function resolveWebHost(host) {
if (host === void 0) return;
if (typeof host === "boolean") return host;
if (typeof host === "string") return host;
return String(host);
}
/**
* @description 为 serve 模式构造统一的小程序开发动作,供热键与命令复用。
*/
function createServeMiniProgramDevActions(options) {
const projectPath = options.projectPath ?? options.fallbackProjectPath;
return {
projectPath,
openIde: async (openOptions = {}) => {
if (!openOptions.forceOpen && await options.tryReuseForwardConsole?.()) return "已通过控制台转发复用当前开发者工具会话";
await options.openIde(projectPath, openOptions);
startBackgroundForwardConsole(options.startForwardConsole, openOptions);
return openOptions.forceReopen ? "已重新打开微信开发者工具项目" : "已打开或复用微信开发者工具项目";
},
rebuild: async () => {
await options.build();
return "已手动重新构建当前小程序产物";
}
};
}
function waitForServeShutdownSignal() {
return new Promise((resolve) => {
const onSignal = () => {
process.off("SIGINT", onSignal);
process.off("SIGTERM", onSignal);
resolve();
};
process.on("SIGINT", onSignal);
process.on("SIGTERM", onSignal);
});
}
//#endregion
//#region src/cli/commands/serve/index.ts
function writePostOpenSeparator() {
process.stdout?.write?.("\n");
}
function registerServeCommand(cli) {
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) => {
filterDuplicateOptions(options);
setCommandNodeEnv("development");
const cwd = root ?? process.cwd();
const configFile = resolveConfigFile(options);
const targets = resolveRuntimeTargets(options);
const webBackend = getBackendForCapability(targets, "web", "dev");
const host = webBackend ? resolveWebHost(options.host) : void 0;
const inlineConfig = createInlineConfig(targets, {
scope: options.scope,
host
});
const ctx = await createCompilerContext({
cwd,
mode: options.mode ?? "development",
isDev: true,
configFile,
inlineConfig,
cliPlatform: targets.rawPlatform,
preloadAppEntry: false,
syncAutoImportSupportFiles: false,
projectConfigPath: options.projectConfig
});
const { configService } = ctx;
const miniBackend = getBackendForCapability(targets, "miniprogram", "dev");
const aiEnvironment = await detectAiDevelopmentEnvironment();
const mcpConfig = applyMcpCliOptions(configService.weappViteConfig?.mcp, options);
logRuntimeTarget(targets, { resolvedConfigPlatform: configService.platform });
const enableAnalyze = Boolean(isUiEnabled(options) && miniBackend);
let analyzeHandle;
const miniProgramDevActions = createServeMiniProgramDevActions({
build: async () => {
await miniBackend?.driver.dev(ctx, options);
},
fallbackProjectPath: configService.cwd,
openIde: async (projectPath, openOptions) => {
const forceReopen = openOptions?.forceReopen === true;
const openStrategy = resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy);
const useAutomatorOpen = openStrategy === "automator";
await openIde(configService.platform, projectPath, {
loginRetry: options.loginRetry,
loginRetryTimeout: options.loginRetryTimeout,
nonInteractive: options.nonInteractive,
openRecovery: false,
prepareAutomatorSession: useAutomatorOpen,
reuseOpenedProject: !forceReopen,
skipAutomatorCompile: !forceReopen,
skipPostOpenHealthCheck: true,
trustProject: options.trustProject,
openStrategy,
useAutomatorOpen
});
writePostOpenSeparator();
},
projectPath: resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd),
startForwardConsole: async (openOptions) => {
if (resolveServeIdeOpenStrategy(openOptions, options.ideOpenStrategy) === "automator") return await maybeStartForwardConsole({
openedOnly: true,
preferOpenedSession: true,
platform: configService.platform,
mpDistRoot: configService.mpDistRoot,
cwd: configService.cwd,
weappViteConfig: configService.weappViteConfig
});
return await maybeStartForwardConsole({
platform: configService.platform,
mpDistRoot: configService.mpDistRoot,
cwd: configService.cwd,
preferOpenedSession: false,
recoverAutomatorSession: async () => {
const projectPath = resolveIdeProjectRoot(configService.mpDistRoot, configService.cwd);
await openIde(configService.platform, projectPath, {
loginRetry: options.loginRetry,
loginRetryTimeout: options.loginRetryTimeout,
nonInteractive: options.nonInteractive,
openRecovery: false,
openStrategy: "automator",
prepareAutomatorSession: true,
reuseOpenedProject: false,
skipAutomatorCompile: false,
skipPostOpenHealthCheck: true,
trustProject: options.trustProject,
useAutomatorOpen: true
});
},
weappViteConfig: configService.weappViteConfig
});
},
tryReuseForwardConsole: async () => {
return await maybeStartForwardConsole({
openedOnly: true,
platform: configService.platform,
mpDistRoot: configService.mpDistRoot,
cwd: configService.cwd,
weappViteConfig: configService.weappViteConfig
});
}
});
const devHotkeysSession = miniBackend ? startDevHotkeys({
cwd: configService.cwd,
agentName: aiEnvironment.agentName,
isAgent: aiEnvironment.isAgent,
mcpConfig,
openIde: async () => await miniProgramDevActions.openIde({
forceOpen: true,
forceReopen: true
}),
platform: configService.platform,
projectPath: miniProgramDevActions.projectPath ?? configService.cwd,
rebuild: miniProgramDevActions.rebuild,
silentStartupHint: true,
weappViteConfig: configService.weappViteConfig
}) : void 0;
try {
const analyzeController = createAnalyzeController({
configFile,
ctx,
options,
targets
});
let webServer;
for (const backend of targets.select("dev")) {
if (backend.descriptor.id === "miniprogram") {
const miniBuildStartedAt = Date.now();
const buildResult = await backend.driver.dev(ctx, options);
const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
logger_default.success(`小程序初次构建完成,耗时:${formatDuration(miniBuildDurationMs)}`);
ctx.autoImportService?.syncSupportFileResolverComponents().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
logger_default.warn(`[prepare] 后台同步 .weapp-vite 支持文件失败:${message}`);
});
if (enableAnalyze) {
await analyzeController.startDashboard(startAnalyzeDashboard);
analyzeHandle = analyzeController.getHandle();
analyzeController.emitRuntimeEvents([{
kind: "build",
level: "success",
title: "mini initial build completed",
detail: "小程序开发态初次构建已完成,dashboard 可继续监听后续刷新。",
durationMs: miniBuildDurationMs,
tags: [
"dev",
"mini",
"initial"
]
}]);
await analyzeController.bindWatcher(buildResult).runInitialUpdate();
}
continue;
}
if (backend.descriptor.id !== "web") continue;
const webServerStartedAt = Date.now();
try {
webServer = await backend.driver.dev(ctx, options);
logger_default.success(`Web 开发服务启动完成,耗时:${formatDuration(Date.now() - webServerStartedAt)}`);
analyzeController.emitRuntimeEvents([{
kind: "system",
level: "success",
title: "web dev server started",
detail: "Web 开发服务器已启动,可与小程序调试 UI 并行工作。",
durationMs: Date.now() - webServerStartedAt,
tags: ["dev", "web"]
}]);
} catch (error) {
analyzeController.emitRuntimeEvents([{
kind: "diagnostic",
level: "error",
title: "web dev server failed",
detail: error instanceof Error ? error.message : String(error),
durationMs: Date.now() - webServerStartedAt,
tags: ["dev", "web"]
}]);
logger_default.error(error);
throw error;
}
}
if (miniBackend) {
logBuildAppFinish(configService, webServer, {
skipWeb: !webBackend,
uiUrls: analyzeHandle?.urls
});
devHotkeysSession?.restore();
} else if (webBackend) logBuildAppFinish(configService, webServer, { skipMini: true });
if (options.open && getBackendForCapability(targets, "miniprogram", "ide")) {
analyzeController.emitRuntimeEvents([{
kind: "command",
level: "info",
title: "opening ide",
detail: "开发服务已就绪,准备打开 IDE 项目。",
tags: ["ide", "open"]
}]);
devHotkeysSession?.suspend();
try {
await miniProgramDevActions.openIde({
forceOpen: true,
forceReopen: false,
openStrategy: options.ideOpenStrategy ?? "cli"
});
} finally {
devHotkeysSession?.restore();
}
}
if (analyzeHandle) await analyzeController.waitForExit();
else if (targets.has("dev")) await waitForServeShutdownSignal();
} finally {
devHotkeysSession?.close();
await closeActiveForwardConsole();
for (const backend of [...targets.select("dev")].reverse()) await backend.driver.close(ctx);
}
});
}
//#endregion
//#region src/cli/error.ts
const watchLimitErrorCodes = /* @__PURE__ */ new Set(["EMFILE", "ENOSPC"]);
const watchLimitMessagePattern = /EMFILE|ENOSPC|unable to start FSEvent stream/i;
function findWatchLimitErrorCode(error) {
const visited = /* @__PURE__ */ new Set();
const visit = (value) => {
if (!value || typeof value !== "object") return;
if (visited.has(value)) return;
visited.add(value);
const maybeError = value;
if (typeof maybeError.code === "string" && watchLimitErrorCodes.has(maybeError.code)) return maybeError.code;
if (typeof maybeError.message === "string" && watchLimitMessagePattern.test(maybeError.message)) {
const match = maybeError.message.match(/EMFILE|ENOSPC/i);
if (match?.[0]) return match[0].toUpperCase();
return "EMFILE";
}
if (Array.isArray(maybeError.errors)) for (const item of maybeError.errors) {
const found = visit(item);
if (found) return found;
}
const fromError = visit(maybeError.error);
if (fromError) return fromError;
return visit(maybeError.cause);
};
return visit(error);
}
function handleCLIError(error) {
const code = findWatchLimitErrorCode(error);
if (code) {
const tip = process.platform === "darwin" ? "可先执行 `ulimit -n 65536` 后重试。" : "请提高系统文件描述符上限后重试。";
logger_default.error(`[监听] 文件监听数量达到上限 (${code}),${tip}`);
return;
}
logger_default.error(error);
}
//#endregion
//#region src/cli/ide.ts
const WEAPP_VITE_NATIVE_COMMANDS = /* @__PURE__ */ new Set([
"dev",
"serve",
"build",
"close",
"analyze",
"alipay",
"init",
"open",
"npm",
"build:npm",
"build-npm",
"generate",
"g",
"mcp"
]);
/**
* @description 让 weapp-vite 在未命中自身命令时,回退到 weapp-ide-cli。
*/
async function tryRunIdeCommand(argv) {
const command = argv[0];
if (!command) return false;
if (command === "ide") {
if (argv[1] === "logs" || argv[1] === "doctor" || argv[1] === "--help" || argv[1] === "-h") return false;
if (!await dispatchWechatCliCommand(argv.slice(1))) await executeWechatIdeCliCommand(argv.slice(1));
return true;
}
if (command.startsWith("-")) return false;
if (command === "help") {
const target = argv[1];
if (!target || WEAPP_VITE_NATIVE_COMMANDS.has(target) || !isWeappIdeTopLevelCommand(target)) return false;
await executeWechatIdeCliCommand(argv);
return true;
}
if (WEAPP_VITE_NATIVE_COMMANDS.has(command) || !isWeappIdeTopLevelCommand(command)) return false;
if (!await dispatchWechatCliCommand(argv)) await executeWechatIdeCliCommand(argv);
return true;
}
//#endregion
//#region src/cli/mcpAutoStart.ts
const SKIP_COMMANDS = /* @__PURE__ */ new Set([
"--help",
"--version",
"-h",
"-v",
"help",
"ide",
"mcp"
]);
const REG_EADDRINUSE = /EADDRINUSE/;
let started = false;
function shouldAutoStartMcp(argv) {
const command = argv[0];
if (!command || command.startsWith("-")) return true;
if (SKIP_COMMANDS.has(command)) return false;
return command.includes("/") || command.includes("\\") || command.startsWith(".");
}
async function maybeAutoStartMcpServer(argv, cliOptions) {
if (started || !shouldAutoStartMcp(argv)) return;
const configFile = resolveConfigFile(cliOptions);
let rawMcpConfig;
try {
rawMcpConfig = (await loadConfig(configFile))?.config?.weapp?.mcp;
} catch (error) {
logger_default.warn(`[mcp] 读取配置失败,使用默认 MCP 配置自动启动:${error instanceof Error ? error.message : String(error)}`);
}
const maybeMcpConfig = rawMcpConfig;
const aiEnvironment = await detectAiDevelopmentEnvironment();
const resolvedMcp = resolveWeappMcpConfig(maybeMcpConfig, {
agentName: aiEnvironment.agentName,
cwd: process.cwd(),
isAgent: aiEnvironment.isAgent
});
if (!resolvedMcp.enabled || !resolvedMcp.autoStart) return;
try {
await startWeappViteMcpServer({
endpoint: resolvedMcp.endpoint,
host: resolvedMcp.host,
port: resolvedMcp.port,
restEndpoint: resolvedMcp.restEndpoint,
quiet: true,
transport: "streamable-http",
unref: true,
workspaceRoot: process.cwd()
});
started = true;
const mcpUrl = `http://${resolvedMcp.host}:${resolvedMcp.port}${resolvedMcp.endpoint}`;
logger_default.success("MCP 服务已自动启动:");
logger_default.info(` ➜ ${colors.cyan(mcpUrl)}`);
if (resolvedMcp.restEndpoint !== false) logger_default.info(` REST ➜ ${colors.cyan(`http://${resolvedMcp.host}:${resolvedMcp.port}${resolvedMcp.restEndpoint}`)}`);
for (const line of formatMcpQuickStart({
httpUrl: mcpUrl,
transport: "http"
})) logger_default.info(line);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (REG_EADDRINUSE.test(message)) {
logger_default.info(`[mcp] 端口 ${resolvedMcp.port} 已被占用,跳过自动启动。`);
started = true;
return;
}
logger_default.warn(`[mcp] 自动启动失败:${message}`);
}
}
//#endregion
//#region src/cli/prepareGuard.ts
function isPrepareCommandArgs(args) {
return Array.isArray(args) && args[0] === "prepare";
}
function formatPrepareSkipMessage(error) {
return `[prepare] 跳过 .weapp-vite 支持文件预生成:${error instanceof Error ? error.message : String(error)}`;
}
function handlePrepareLifecycleError(args, error) {
if (!isPrepareCommandArgs(args)) return false;
logger_default.warn(formatPrepareSkipMessage(error));
process.exitCode = 0;
return true;
}
//#endregion
//#region src/cli.ts
const cli = cac("weapp-vite");
try {
checkRuntime({
bun: "0.0.0",
deno: "0.0.0",
node: "^20.19.0 || >=22.12.0"
});
} catch {}
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`);
registerIdeCommand(cli);
registerAlipayCommand(cli);
registerBuildCommand(cli);
registerCloseCommand(cli);
registerAnalyzeCommand(cli);
registerInitCommand(cli);
registerOpenCommand(cli);
registerPrepareCommand(cli);
registerNpmCommand(cli);
registerGenerateCommand(cli);
registerMcpCommand(cli);
registerServeCommand(cli);
cli.help();
cli.version(VERSION);
const skipManagedTsconfigBootstrapCommands = /* @__PURE__ */ new Set([
"g",
"generate",
"ide",
"init",
"mcp",
"npm",
"alipay"
]);
function resolveManagedTsconfigBootstrapRoot(args) {
const [firstArg, secondArg] = args;
if (!firstArg || firstArg === "--help" || firstArg === "-h" || firstArg === "--version" || firstArg === "-v") return;
if (firstArg.startsWith("-")) return process.cwd();
if (skipManagedTsconfigBootstrapCommands.has(firstArg)) {
if (firstArg === "ide") {
const thirdArg = args[2];
if (secondArg === "logs" && thirdArg && !thirdArg.startsWith("-")) return path.resolve(thirdArg);
}
return;
}
if ([
"analyze",
"build",
"close",
"dev",
"open",
"prepare",
"serve"
].includes(firstArg)) {
if (secondArg && !secondArg.startsWith("-")) return path.resolve(secondArg);
return process.cwd();
}
return path.resolve(firstArg);
}
try {
const args = process.argv.slice(2);
if (!await tryRunIdeCommand(args)) {
const managedTsconfigBootstrapRoot = resolveManagedTsconfigBootstrapRoot(args);
if (managedTsconfigBootstrapRoot) await syncManagedTsconfigBootstrapFiles(managedTsconfigBootstrapRoot);
cli.parse(process.argv, { run: false });
await maybeAutoStartMcpServer(args, cli.options);
await cli.runMatchedCommand();
}
} catch (error) {
if (!handlePrepareLifecycleError(process.argv.slice(2), error)) {
handleCLIError(error);
process.exitCode = 1;
}
}
//#endregion
export {};