const path = require("node:path"); const { spawnSync } = require("node:child_process"); const projectRoot = path.resolve(__dirname, ".."); const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; const electronBuilder = path.join( projectRoot, "node_modules", ".bin", process.platform === "win32" ? "electron-builder.cmd" : "electron-builder", ); function usage() { console.log(` 客户端打包脚本(不包含认证服务/管理后台) 用法: npm.cmd run build:client:win -- --auth-url=https://auth.example.com npm.cmd run build:client:win -- --auth-url=http://127.0.0.1:3302 --dry-run 参数: --auth-url 认证服务地址,可带或不带 /api --dir 只打包目录,不生成安装包 --dry-run 只打印将执行的命令,不真正打包 --help 显示帮助 `); } function readArg(name) { const prefix = `${name}=`; const inline = process.argv.find(arg => arg.startsWith(prefix)); if (inline) return inline.slice(prefix.length); const index = process.argv.indexOf(name); if (index >= 0) return process.argv[index + 1]; return ""; } function hasArg(name) { return process.argv.includes(name); } function normalizeAuthUrl(value) { const raw = String(value || "").trim(); if (!raw) return ""; if (/^https?\/\//i.test(raw)) { throw new Error(`认证服务地址无效,协议后缺少冒号:${raw}`); } let parsed; try { parsed = new URL(raw); } catch { throw new Error(`认证服务地址无效:${raw}`); } if (!["http:", "https:"].includes(parsed.protocol)) { throw new Error(`认证服务地址只支持 http 或 https:${raw}`); } if (["http", "https"].includes(parsed.hostname) && parsed.pathname.startsWith("//")) { throw new Error(`认证服务地址疑似多写了协议,请检查:${raw}`); } if (!parsed.hostname || parsed.hostname.includes("/")) { throw new Error(`认证服务地址主机名无效:${raw}`); } parsed.pathname = parsed.pathname.replace(/\/api\/?$/i, "").replace(/\/+$/, ""); parsed.search = ""; parsed.hash = ""; return parsed.toString().replace(/\/+$/, ""); } function quoteDisplay(value) { const text = String(value); return /\s/.test(text) ? `"${text}"` : text; } function quoteCmdArg(value) { const text = String(value); if (!/[ \t"&|<>^]/.test(text)) return text; return `"${text.replace(/"/g, '""')}"`; } function isWindowsCmd(command) { return process.platform === "win32" && /\.(cmd|bat)$/i.test(command); } function run(command, args, env, dryRun) { const display = [command, ...args].map(quoteDisplay).join(" "); console.log(`[package-client] ${display}`); if (dryRun) return; const spawnCommand = isWindowsCmd(command) ? (process.env.ComSpec || "cmd.exe") : command; const spawnArgs = isWindowsCmd(command) ? ["/d", "/s", "/c", [quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ")] : args; const result = spawnSync(spawnCommand, spawnArgs, { cwd: projectRoot, stdio: "inherit", env, shell: false, }); if (result.error) { throw result.error; } if (result.status !== 0) { throw new Error(`命令执行失败:${display}`); } } function main() { if (hasArg("--help") || hasArg("-h")) { usage(); return; } const authUrl = normalizeAuthUrl( readArg("--auth-url") || process.env.PACKAGE_AUTH_SERVER_URL || process.env.AUTH_SERVER_URL || process.env.VITE_AUTH_SERVER_URL, ); const dryRun = hasArg("--dry-run"); const dirOnly = hasArg("--dir"); if (!authUrl) { usage(); throw new Error("打包客户端必须指定认证服务地址:--auth-url=https://auth.example.com"); } const env = { ...process.env, AUTH_SERVER_URL: authUrl, VITE_AUTH_SERVER_URL: authUrl, PACKAGE_CLIENT_ONLY: "1", }; console.log(`[package-client] 认证服务地址:${authUrl}`); console.log("[package-client] 本脚本只打包客户端,不会启动或打包本地认证服务。"); run(npmCommand, ["run", "prepare-package"], env, dryRun); run(npmCommand, ["run", "build"], env, dryRun); run(process.execPath, ["scripts/build-resource-artifacts.cjs"], env, dryRun); run(electronBuilder, ["--win", "--x64", ...(dirOnly ? ["--dir"] : [])], env, dryRun); if (!dirOnly) { run(process.execPath, ["scripts/fix-latest-yml.cjs"], env, dryRun); run(process.execPath, ["scripts/assemble-release-artifacts.cjs"], env, dryRun); } } try { main(); } catch (error) { console.error(`[package-client] ${error.message}`); process.exit(1); }