zy-gbs-cmd/src/push.js

228 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const childProcess = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
const readline = require('readline/promises');
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' });
}
function readProjectConfig(projectRoot) {
const configPath = path.join(projectRoot, CONFIG_FILE_NAME);
if (!fs.existsSync(configPath)) {
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]);
if (missingFields.length) {
throw new Error(`${CONFIG_FILE_NAME} 缺少字段: ${missingFields.join(', ')}`);
}
return config;
}
function readPackageJson(projectRoot) {
const packagePath = path.join(projectRoot, 'package.json');
if (!fs.existsSync(packagePath)) {
throw new Error('未找到 package.json');
}
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
}
async function selectOption(input, message, options) {
// 只有一个选项时直接返回,避免无意义的终端交互。
if (options.length === 1) {
return options[0];
}
console.log(message);
options.forEach((option, index) => console.log(` ${index + 1}. ${option}`));
while (true) {
const answer = await input.question('请输入序号: ');
const selected = options[Number(answer) - 1];
if (selected) {
return selected;
}
console.log('请输入有效序号。');
}
}
async function resolveEnvironment(input, config, environmentArgument) {
// 环境名称以 jjb.config.js 的 environment 键为准,不在 CLI 中写死。
const environments = config.environment && typeof config.environment === 'object'
? Object.keys(config.environment)
: [];
if (!environments.length) {
return null;
}
if (environmentArgument) {
if (!environments.includes(environmentArgument)) {
throw new Error(`未找到环境 ${environmentArgument}`);
}
return environmentArgument;
}
return selectOption(input, '请选择环境:', environments);
}
function resolveBuildScript(buildScripts, environment) {
// 显式传入 production 时只允许使用 build:production防止构建错环境。
const buildScript = `build:${environment}`;
if (!buildScripts.includes(buildScript)) {
throw new Error(`package.json 中没有构建脚本 ${buildScript}`);
}
return buildScript;
}
function ensureDependencies(projectRoot) {
// 业务项目没有 node_modules 时才安装依赖,并且始终使用 Yarn。
if (fs.existsSync(path.join(projectRoot, 'node_modules'))) {
return;
}
console.log('未检测到 node_modules正在安装依赖。');
runYarn(['install'], projectRoot);
}
function copyBuildOutput(projectRoot, javaRepository, appIdentifier) {
const distPath = path.join(projectRoot, 'dist');
const indexPath = path.join(distPath, 'index.html');
const applicationPath = path.join(distPath, appIdentifier);
if (!fs.existsSync(indexPath)) {
throw new Error('构建产物缺少 dist/index.html');
}
if (!fs.existsSync(applicationPath)) {
throw new Error(`构建产物缺少 dist/${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/<appIdentifier>/ 复制到临时 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 =>
name.toLowerCase().includes('build')
);
if (!buildScripts.length) {
throw new Error('package.json 中没有名称包含 build 的脚本');
}
const input = readline.createInterface({ input: process.stdin, output: process.stdout });
let temporaryRoot;
try {
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);
console.log(`正在克隆 Java 仓库: ${config.javaGit}`);
run('git', ['clone', config.javaGit, javaRepository], temporaryRoot);
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,
encoding: 'utf8',
}).trim();
if (!changedFiles) {
// 没有差异时不创建空提交,也不执行远程推送。
console.log('构建产物没有变化,无需提交。');
return;
}
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('代码推送完成。');
} finally {
input.close();
if (temporaryRoot) {
try {
// 第 8 步:成功或失败都会删除临时 Java 仓库;清理失败只告警,不覆盖原始错误。
fs.rmSync(temporaryRoot, { recursive: true, force: true });
} catch (error) {
console.warn(`临时目录清理失败: ${error.message}`);
}
}
}
}
module.exports = pushJava;
module.exports.resolveBuildScript = resolveBuildScript;