diff --git a/bin/command.js b/bin/command.js index 3bafc8c..dfbefef 100644 --- a/bin/command.js +++ b/bin/command.js @@ -2,6 +2,14 @@ const { version } = require('../package.json'); +/* + * 命令启动入口: + * 1. package.json 的 bin 配置把全局命令 zy-gbs-cmd 指向本文件。 + * 2. 用户执行 zy-gbs-cmd push java production。 + * 3. 本文件从 process.argv 取出 push、java、production。 + * 4. 确认是 push java 后,调用 src/push.js 开始真正的构建和推送。 + */ + function printHelp() { console.log(`zy-gbs-cmd @@ -13,6 +21,7 @@ function printHelp() { } async function main() { + // 第 1 步:解析命令。process.argv 前两项是 Node.js 路径和本文件路径,从第三项开始才是用户参数。 const [, , command, argument, environment] = process.argv; if (!command || command === 'help') { @@ -26,6 +35,7 @@ async function main() { } if (command === 'push' && argument === 'java') { + // 第 2 步:进入推送主流程。production 会作为 environment 传给 pushJava。 await require('../src/push')({ environment }); return; } @@ -35,6 +45,7 @@ async function main() { } main().catch(error => { + // 统一设置非零退出码,便于终端和 CI 判断命令执行失败。 console.error(`推送失败: ${error.message}`); process.exitCode = 1; }); diff --git a/src/package-manager.js b/src/package-manager.js index 25f8209..045a2a8 100644 --- a/src/package-manager.js +++ b/src/package-manager.js @@ -1,10 +1,12 @@ const childProcess = require('child_process'); function getExecutable(command) { + // Windows 全局安装的命令通常是 .cmd 文件,其他系统直接使用命令名。 return process.platform === 'win32' ? `${command}.cmd` : command; } function commandExists(command) { + // 使用系统自带的命令查找能力,避免为了检测 Yarn 引入额外依赖。 const lookupCommand = process.platform === 'win32' ? 'where.exe' : 'which'; const result = childProcess.spawnSync(lookupCommand, [getExecutable(command)], { stdio: 'ignore', @@ -13,6 +15,7 @@ function commandExists(command) { } function ensureYarnAvailable(isCommandAvailable = commandExists) { + // 参数可注入是为了让测试覆盖 Yarn 存在和不存在两种情况。 if (!isCommandAvailable('yarn')) { throw new Error('未检测到 Yarn,请先安装 Yarn'); } @@ -23,6 +26,7 @@ function runYarn(args, cwd) { const executable = getExecutable('yarn'); console.log(`执行: yarn ${args.join(' ')}`); if (process.platform === 'win32') { + // Node.js 不能始终直接执行 yarn.cmd,因此交给 cmd.exe 解析,避免 spawnSync yarn ENOENT。 childProcess.execFileSync(process.env.ComSpec || 'cmd.exe', [ '/d', '/s', diff --git a/src/push.js b/src/push.js index cf20be1..cd09de0 100644 --- a/src/push.js +++ b/src/push.js @@ -7,9 +7,11 @@ const { ensureYarnAvailable, runYarn } = require('./package-manager'); const DEFAULT_BRANCH = 'master'; const CONFIG_FILE_NAME = 'jjb.config.js'; +// 前端构建产物在 Java 仓库中的固定部署目录。 const JAVA_TEMPLATE_PATH = path.join('start', 'src', 'main', 'resources', 'templates'); function run(command, args, cwd) { + // Git 命令同步执行并继承终端输出,失败时由 execFileSync 直接抛出异常。 console.log(`执行: ${command} ${args.join(' ')}`); childProcess.execFileSync(command, args, { cwd, stdio: 'inherit' }); } @@ -20,8 +22,10 @@ function readProjectConfig(projectRoot) { throw new Error(`未找到 ${CONFIG_FILE_NAME}`); } + // 清除 Node.js 模块缓存,确保每次执行都读取最新的业务项目配置。 delete require.cache[require.resolve(configPath)]; const importedConfig = require(configPath); + // 同时兼容 CommonJS 导出和经过转译后位于 default 中的配置。 const config = importedConfig.default || importedConfig; const requiredFields = ['javaGit', 'javaGitName', 'appIdentifier']; const missingFields = requiredFields.filter(field => !config[field]); @@ -43,6 +47,7 @@ function readPackageJson(projectRoot) { } async function selectOption(input, message, options) { + // 只有一个选项时直接返回,避免无意义的终端交互。 if (options.length === 1) { return options[0]; } @@ -61,6 +66,7 @@ async function selectOption(input, message, options) { } async function resolveEnvironment(input, config, environmentArgument) { + // 环境名称以 jjb.config.js 的 environment 键为准,不在 CLI 中写死。 const environments = config.environment && typeof config.environment === 'object' ? Object.keys(config.environment) : []; @@ -80,6 +86,7 @@ async function resolveEnvironment(input, config, environmentArgument) { } function resolveBuildScript(buildScripts, environment) { + // 显式传入 production 时只允许使用 build:production,防止构建错环境。 const buildScript = `build:${environment}`; if (!buildScripts.includes(buildScript)) { throw new Error(`package.json 中没有构建脚本 ${buildScript}`); @@ -88,6 +95,7 @@ function resolveBuildScript(buildScripts, environment) { } function ensureDependencies(projectRoot) { + // 业务项目没有 node_modules 时才安装依赖,并且始终使用 Yarn。 if (fs.existsSync(path.join(projectRoot, 'node_modules'))) { return; } @@ -111,13 +119,28 @@ function copyBuildOutput(projectRoot, javaRepository, appIdentifier) { const templateRoot = path.join(javaRepository, JAVA_TEMPLATE_PATH); const targetApplicationPath = path.join(templateRoot, appIdentifier); fs.mkdirSync(templateRoot, { recursive: true }); + // 先删除旧应用目录再整体复制,避免已从新构建中删除的静态文件残留。 fs.rmSync(targetApplicationPath, { recursive: true, force: true }); fs.cpSync(applicationPath, targetApplicationPath, { recursive: true }); fs.copyFileSync(indexPath, path.join(templateRoot, `${appIdentifier}.html`)); } +/* + * zy-gbs-cmd push java production 的主执行链: + * 1. 把执行命令的当前目录当作前端业务项目。 + * 2. 读取业务项目的 package.json 和 jjb.config.js。 + * 3. 将 production 匹配成 build:production,并读取 production 对应的 Java 分支。 + * 4. 把 jjb.config.js 中的 javaGit 克隆到系统临时目录。 + * 5. 在前端业务项目中执行 yarn run build:production。 + * 6. 把 dist/index.html 和 dist// 复制到临时 Java 仓库。 + * 7. 在临时 Java 仓库中执行 git add、git commit、git push。 + * 8. 删除临时 Java 仓库。 + */ async function pushJava({ environment: environmentArgument } = {}) { + // 第 1 步:命令必须在业务项目根目录执行,process.cwd() 就是用户执行命令时所在的目录。 const projectRoot = process.cwd(); + + // 第 2 步:从业务项目读取构建脚本和 Java 仓库配置,不读取 zy-gbs-cmd 自己的配置。 const packageJson = readPackageJson(projectRoot); const config = readProjectConfig(projectRoot); const buildScripts = Object.keys(packageJson.scripts || {}).filter(name => @@ -135,18 +158,23 @@ async function pushJava({ environment: environmentArgument } = {}) { let buildScript; let environment; + // 第 3 步:确定本次使用哪个构建脚本和环境。 if (environmentArgument) { + // 例如 production 会自动匹配 build:production,不再要求用户选择。 environment = await resolveEnvironment(input, config, environmentArgument); buildScript = resolveBuildScript(buildScripts, environment); } else { + // 未带环境参数时交互选择。 buildScript = await selectOption(input, '请选择构建命令:', buildScripts); environment = await resolveEnvironment(input, config); } const environmentConfig = environment ? config.environment[environment] || {} : {}; + // 第 4 步准备:分支优先读取 environment.production.javaGitBranch,最后才回退到 master。 const branch = environmentConfig.javaGitBranch || config.javaGitBranch || DEFAULT_BRANCH; ensureYarnAvailable(); + // 第 4 步:创建临时目录,并把 config.javaGit 指向的 Java 仓库克隆进去。 temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zy-gbs-cmd-java-')); const javaRepository = path.join(temporaryRoot, config.javaGitName); @@ -155,10 +183,14 @@ async function pushJava({ environment: environmentArgument } = {}) { run('git', ['checkout', branch], javaRepository); run('git', ['pull'], javaRepository); + // 第 5 步:回到前端业务项目安装依赖并构建,Java 仓库中不会执行 Yarn。 ensureDependencies(projectRoot); runYarn(['run', buildScript], projectRoot); + + // 第 6 步:把前端 dist 产物复制到 Java 仓库的 resources/templates 目录。 copyBuildOutput(projectRoot, javaRepository, config.appIdentifier); + // 第 7 步:下面所有 Git 命令都在临时 Java 仓库中执行。 run('git', ['add', '.'], javaRepository); const changedFiles = childProcess.execFileSync('git', ['status', '--porcelain'], { cwd: javaRepository, @@ -166,6 +198,7 @@ async function pushJava({ environment: environmentArgument } = {}) { }).trim(); if (!changedFiles) { + // 没有差异时不创建空提交,也不执行远程推送。 console.log('构建产物没有变化,无需提交。'); return; } @@ -173,6 +206,7 @@ async function pushJava({ environment: environmentArgument } = {}) { const defaultMessage = `zy-gbs-cmd: update ${config.appIdentifier}`; const message = (await input.question(`请输入提交信息 (${defaultMessage}): `)).trim() || defaultMessage; + // Git 认证和提交作者均由本机 Git 配置提供,工具本身不保存凭据或写死 author。 run('git', ['commit', '-m', message, '--no-verify'], javaRepository); run('git', ['push'], javaRepository); console.log('代码推送完成。'); @@ -180,6 +214,7 @@ async function pushJava({ environment: environmentArgument } = {}) { input.close(); if (temporaryRoot) { try { + // 第 8 步:成功或失败都会删除临时 Java 仓库;清理失败只告警,不覆盖原始错误。 fs.rmSync(temporaryRoot, { recursive: true, force: true }); } catch (error) { console.warn(`临时目录清理失败: ${error.message}`);