228 lines
8.9 KiB
JavaScript
228 lines
8.9 KiB
JavaScript
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;
|