feat(push): 代码和注释均增加,提升可维护性与可读性

master
LiuJiaNan 2026-08-26 09:10:45 +08:00
parent 13f8f61b1a
commit 240caf3c66
3 changed files with 50 additions and 0 deletions

View File

@ -2,6 +2,14 @@
const { version } = require('../package.json'); const { version } = require('../package.json');
/*
* 命令启动入口
* 1. package.json bin 配置把全局命令 zy-gbs-cmd 指向本文件
* 2. 用户执行 zy-gbs-cmd push java production
* 3. 本文件从 process.argv 取出 pushjavaproduction
* 4. 确认是 push java 调用 src/push.js 开始真正的构建和推送
*/
function printHelp() { function printHelp() {
console.log(`zy-gbs-cmd <command> console.log(`zy-gbs-cmd <command>
@ -13,6 +21,7 @@ function printHelp() {
} }
async function main() { async function main() {
// 第 1 步解析命令。process.argv 前两项是 Node.js 路径和本文件路径,从第三项开始才是用户参数。
const [, , command, argument, environment] = process.argv; const [, , command, argument, environment] = process.argv;
if (!command || command === 'help') { if (!command || command === 'help') {
@ -26,6 +35,7 @@ async function main() {
} }
if (command === 'push' && argument === 'java') { if (command === 'push' && argument === 'java') {
// 第 2 步进入推送主流程。production 会作为 environment 传给 pushJava。
await require('../src/push')({ environment }); await require('../src/push')({ environment });
return; return;
} }
@ -35,6 +45,7 @@ async function main() {
} }
main().catch(error => { main().catch(error => {
// 统一设置非零退出码,便于终端和 CI 判断命令执行失败。
console.error(`推送失败: ${error.message}`); console.error(`推送失败: ${error.message}`);
process.exitCode = 1; process.exitCode = 1;
}); });

View File

@ -1,10 +1,12 @@
const childProcess = require('child_process'); const childProcess = require('child_process');
function getExecutable(command) { function getExecutable(command) {
// Windows 全局安装的命令通常是 .cmd 文件,其他系统直接使用命令名。
return process.platform === 'win32' ? `${command}.cmd` : command; return process.platform === 'win32' ? `${command}.cmd` : command;
} }
function commandExists(command) { function commandExists(command) {
// 使用系统自带的命令查找能力,避免为了检测 Yarn 引入额外依赖。
const lookupCommand = process.platform === 'win32' ? 'where.exe' : 'which'; const lookupCommand = process.platform === 'win32' ? 'where.exe' : 'which';
const result = childProcess.spawnSync(lookupCommand, [getExecutable(command)], { const result = childProcess.spawnSync(lookupCommand, [getExecutable(command)], {
stdio: 'ignore', stdio: 'ignore',
@ -13,6 +15,7 @@ function commandExists(command) {
} }
function ensureYarnAvailable(isCommandAvailable = commandExists) { function ensureYarnAvailable(isCommandAvailable = commandExists) {
// 参数可注入是为了让测试覆盖 Yarn 存在和不存在两种情况。
if (!isCommandAvailable('yarn')) { if (!isCommandAvailable('yarn')) {
throw new Error('未检测到 Yarn请先安装 Yarn'); throw new Error('未检测到 Yarn请先安装 Yarn');
} }
@ -23,6 +26,7 @@ function runYarn(args, cwd) {
const executable = getExecutable('yarn'); const executable = getExecutable('yarn');
console.log(`执行: yarn ${args.join(' ')}`); console.log(`执行: yarn ${args.join(' ')}`);
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Node.js 不能始终直接执行 yarn.cmd因此交给 cmd.exe 解析,避免 spawnSync yarn ENOENT。
childProcess.execFileSync(process.env.ComSpec || 'cmd.exe', [ childProcess.execFileSync(process.env.ComSpec || 'cmd.exe', [
'/d', '/d',
'/s', '/s',

View File

@ -7,9 +7,11 @@ const { ensureYarnAvailable, runYarn } = require('./package-manager');
const DEFAULT_BRANCH = 'master'; const DEFAULT_BRANCH = 'master';
const CONFIG_FILE_NAME = 'jjb.config.js'; const CONFIG_FILE_NAME = 'jjb.config.js';
// 前端构建产物在 Java 仓库中的固定部署目录。
const JAVA_TEMPLATE_PATH = path.join('start', 'src', 'main', 'resources', 'templates'); const JAVA_TEMPLATE_PATH = path.join('start', 'src', 'main', 'resources', 'templates');
function run(command, args, cwd) { function run(command, args, cwd) {
// Git 命令同步执行并继承终端输出,失败时由 execFileSync 直接抛出异常。
console.log(`执行: ${command} ${args.join(' ')}`); console.log(`执行: ${command} ${args.join(' ')}`);
childProcess.execFileSync(command, args, { cwd, stdio: 'inherit' }); childProcess.execFileSync(command, args, { cwd, stdio: 'inherit' });
} }
@ -20,8 +22,10 @@ function readProjectConfig(projectRoot) {
throw new Error(`未找到 ${CONFIG_FILE_NAME}`); throw new Error(`未找到 ${CONFIG_FILE_NAME}`);
} }
// 清除 Node.js 模块缓存,确保每次执行都读取最新的业务项目配置。
delete require.cache[require.resolve(configPath)]; delete require.cache[require.resolve(configPath)];
const importedConfig = require(configPath); const importedConfig = require(configPath);
// 同时兼容 CommonJS 导出和经过转译后位于 default 中的配置。
const config = importedConfig.default || importedConfig; const config = importedConfig.default || importedConfig;
const requiredFields = ['javaGit', 'javaGitName', 'appIdentifier']; const requiredFields = ['javaGit', 'javaGitName', 'appIdentifier'];
const missingFields = requiredFields.filter(field => !config[field]); const missingFields = requiredFields.filter(field => !config[field]);
@ -43,6 +47,7 @@ function readPackageJson(projectRoot) {
} }
async function selectOption(input, message, options) { async function selectOption(input, message, options) {
// 只有一个选项时直接返回,避免无意义的终端交互。
if (options.length === 1) { if (options.length === 1) {
return options[0]; return options[0];
} }
@ -61,6 +66,7 @@ async function selectOption(input, message, options) {
} }
async function resolveEnvironment(input, config, environmentArgument) { async function resolveEnvironment(input, config, environmentArgument) {
// 环境名称以 jjb.config.js 的 environment 键为准,不在 CLI 中写死。
const environments = config.environment && typeof config.environment === 'object' const environments = config.environment && typeof config.environment === 'object'
? Object.keys(config.environment) ? Object.keys(config.environment)
: []; : [];
@ -80,6 +86,7 @@ async function resolveEnvironment(input, config, environmentArgument) {
} }
function resolveBuildScript(buildScripts, environment) { function resolveBuildScript(buildScripts, environment) {
// 显式传入 production 时只允许使用 build:production防止构建错环境。
const buildScript = `build:${environment}`; const buildScript = `build:${environment}`;
if (!buildScripts.includes(buildScript)) { if (!buildScripts.includes(buildScript)) {
throw new Error(`package.json 中没有构建脚本 ${buildScript}`); throw new Error(`package.json 中没有构建脚本 ${buildScript}`);
@ -88,6 +95,7 @@ function resolveBuildScript(buildScripts, environment) {
} }
function ensureDependencies(projectRoot) { function ensureDependencies(projectRoot) {
// 业务项目没有 node_modules 时才安装依赖,并且始终使用 Yarn。
if (fs.existsSync(path.join(projectRoot, 'node_modules'))) { if (fs.existsSync(path.join(projectRoot, 'node_modules'))) {
return; return;
} }
@ -111,13 +119,28 @@ function copyBuildOutput(projectRoot, javaRepository, appIdentifier) {
const templateRoot = path.join(javaRepository, JAVA_TEMPLATE_PATH); const templateRoot = path.join(javaRepository, JAVA_TEMPLATE_PATH);
const targetApplicationPath = path.join(templateRoot, appIdentifier); const targetApplicationPath = path.join(templateRoot, appIdentifier);
fs.mkdirSync(templateRoot, { recursive: true }); fs.mkdirSync(templateRoot, { recursive: true });
// 先删除旧应用目录再整体复制,避免已从新构建中删除的静态文件残留。
fs.rmSync(targetApplicationPath, { recursive: true, force: true }); fs.rmSync(targetApplicationPath, { recursive: true, force: true });
fs.cpSync(applicationPath, targetApplicationPath, { recursive: true }); fs.cpSync(applicationPath, targetApplicationPath, { recursive: true });
fs.copyFileSync(indexPath, path.join(templateRoot, `${appIdentifier}.html`)); 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/<appIdentifier>/ Java
* 7. 在临时 Java 仓库中执行 git addgit commitgit push
* 8. 删除临时 Java 仓库
*/
async function pushJava({ environment: environmentArgument } = {}) { async function pushJava({ environment: environmentArgument } = {}) {
// 第 1 步命令必须在业务项目根目录执行process.cwd() 就是用户执行命令时所在的目录。
const projectRoot = process.cwd(); const projectRoot = process.cwd();
// 第 2 步:从业务项目读取构建脚本和 Java 仓库配置,不读取 zy-gbs-cmd 自己的配置。
const packageJson = readPackageJson(projectRoot); const packageJson = readPackageJson(projectRoot);
const config = readProjectConfig(projectRoot); const config = readProjectConfig(projectRoot);
const buildScripts = Object.keys(packageJson.scripts || {}).filter(name => const buildScripts = Object.keys(packageJson.scripts || {}).filter(name =>
@ -135,18 +158,23 @@ async function pushJava({ environment: environmentArgument } = {}) {
let buildScript; let buildScript;
let environment; let environment;
// 第 3 步:确定本次使用哪个构建脚本和环境。
if (environmentArgument) { if (environmentArgument) {
// 例如 production 会自动匹配 build:production不再要求用户选择。
environment = await resolveEnvironment(input, config, environmentArgument); environment = await resolveEnvironment(input, config, environmentArgument);
buildScript = resolveBuildScript(buildScripts, environment); buildScript = resolveBuildScript(buildScripts, environment);
} else { } else {
// 未带环境参数时交互选择。
buildScript = await selectOption(input, '请选择构建命令:', buildScripts); buildScript = await selectOption(input, '请选择构建命令:', buildScripts);
environment = await resolveEnvironment(input, config); environment = await resolveEnvironment(input, config);
} }
const environmentConfig = environment ? config.environment[environment] || {} : {}; const environmentConfig = environment ? config.environment[environment] || {} : {};
// 第 4 步准备:分支优先读取 environment.production.javaGitBranch最后才回退到 master。
const branch = environmentConfig.javaGitBranch || config.javaGitBranch || DEFAULT_BRANCH; const branch = environmentConfig.javaGitBranch || config.javaGitBranch || DEFAULT_BRANCH;
ensureYarnAvailable(); ensureYarnAvailable();
// 第 4 步:创建临时目录,并把 config.javaGit 指向的 Java 仓库克隆进去。
temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zy-gbs-cmd-java-')); temporaryRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'zy-gbs-cmd-java-'));
const javaRepository = path.join(temporaryRoot, config.javaGitName); const javaRepository = path.join(temporaryRoot, config.javaGitName);
@ -155,10 +183,14 @@ async function pushJava({ environment: environmentArgument } = {}) {
run('git', ['checkout', branch], javaRepository); run('git', ['checkout', branch], javaRepository);
run('git', ['pull'], javaRepository); run('git', ['pull'], javaRepository);
// 第 5 步回到前端业务项目安装依赖并构建Java 仓库中不会执行 Yarn。
ensureDependencies(projectRoot); ensureDependencies(projectRoot);
runYarn(['run', buildScript], projectRoot); runYarn(['run', buildScript], projectRoot);
// 第 6 步:把前端 dist 产物复制到 Java 仓库的 resources/templates 目录。
copyBuildOutput(projectRoot, javaRepository, config.appIdentifier); copyBuildOutput(projectRoot, javaRepository, config.appIdentifier);
// 第 7 步:下面所有 Git 命令都在临时 Java 仓库中执行。
run('git', ['add', '.'], javaRepository); run('git', ['add', '.'], javaRepository);
const changedFiles = childProcess.execFileSync('git', ['status', '--porcelain'], { const changedFiles = childProcess.execFileSync('git', ['status', '--porcelain'], {
cwd: javaRepository, cwd: javaRepository,
@ -166,6 +198,7 @@ async function pushJava({ environment: environmentArgument } = {}) {
}).trim(); }).trim();
if (!changedFiles) { if (!changedFiles) {
// 没有差异时不创建空提交,也不执行远程推送。
console.log('构建产物没有变化,无需提交。'); console.log('构建产物没有变化,无需提交。');
return; return;
} }
@ -173,6 +206,7 @@ async function pushJava({ environment: environmentArgument } = {}) {
const defaultMessage = `zy-gbs-cmd: update ${config.appIdentifier}`; const defaultMessage = `zy-gbs-cmd: update ${config.appIdentifier}`;
const message = (await input.question(`请输入提交信息 (${defaultMessage}): `)).trim() const message = (await input.question(`请输入提交信息 (${defaultMessage}): `)).trim()
|| defaultMessage; || defaultMessage;
// Git 认证和提交作者均由本机 Git 配置提供,工具本身不保存凭据或写死 author。
run('git', ['commit', '-m', message, '--no-verify'], javaRepository); run('git', ['commit', '-m', message, '--no-verify'], javaRepository);
run('git', ['push'], javaRepository); run('git', ['push'], javaRepository);
console.log('代码推送完成。'); console.log('代码推送完成。');
@ -180,6 +214,7 @@ async function pushJava({ environment: environmentArgument } = {}) {
input.close(); input.close();
if (temporaryRoot) { if (temporaryRoot) {
try { try {
// 第 8 步:成功或失败都会删除临时 Java 仓库;清理失败只告警,不覆盖原始错误。
fs.rmSync(temporaryRoot, { recursive: true, force: true }); fs.rmSync(temporaryRoot, { recursive: true, force: true });
} catch (error) { } catch (error) {
console.warn(`临时目录清理失败: ${error.message}`); console.warn(`临时目录清理失败: ${error.message}`);