commit 13f8f61b1a82a71695f928592dae6ff92206e821 Author: LiuJiaNan <15703339975@163.com> Date: Tue Aug 25 17:17:36 2026 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c6d386 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +node_modules/ +*.tgz diff --git a/README.md b/README.md new file mode 100644 index 0000000..b41a655 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# zy-gbs-cmd + +`zy-gbs-cmd` 用于构建前端项目,并将构建产物提交到业务项目配置的 Java Git 仓库。 + +工具不调用授权接口,也不保存账号密码。Git 克隆和推送使用当前电脑已有的 Git 凭据。 + +## 环境要求 + +- Node.js 18 或更高版本 +- Git +- Yarn + +`src/push.js` 使用的模块均为 Node.js 内置模块,无需安装运行时依赖。 + +业务项目的依赖安装和项目构建只使用 Yarn;Windows 下会自动调用 `yarn.cmd`。 + +## 安装 + +安装已发布版本可以使用 npm 或 Yarn: + +```bash +npm install -g zy-gbs-cmd +``` + +```bash +yarn global add zy-gbs-cmd +``` + +本地安装当前目录: + +```bash +npm install -g +``` + +## 业务项目配置 + +在业务项目根目录创建或修改 `jjb.config.js`: + +`javaGit` 必须是完整的 Git 克隆地址,不能只填写 Git 服务地址。 + +## 使用 + +在业务项目根目录执行: + +```bash +zy-gbs-cmd push java +``` + +也可以直接指定环境: + +```bash +zy-gbs-cmd push java production +``` + +指定环境后会自动执行同名构建脚本,例如 `production` 对应 `build:production`,不再询问构建命令和环境。 + +工具会把: + +```text +dist/index.html +dist// +``` + +复制到 Java 仓库: + +```text +start/src/main/resources/templates/.html +start/src/main/resources/templates// +``` + +## 发布到 npm Registry + +登录 Registry: + +```bash +yarn login +``` + +首次发布: + +```bash +yarn publish +``` + +后续发布前需要先更新版本号,例如: + +```bash +yarn version --patch +yarn publish +``` diff --git a/bin/command.js b/bin/command.js new file mode 100644 index 0000000..3bafc8c --- /dev/null +++ b/bin/command.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +const { version } = require('../package.json'); + +function printHelp() { + console.log(`zy-gbs-cmd + +使用: + zy-gbs-cmd help 查看帮助 + zy-gbs-cmd v 查看版本 + zy-gbs-cmd push java [environment] 构建并推送到 Java Git 仓库 +`); +} + +async function main() { + const [, , command, argument, environment] = process.argv; + + if (!command || command === 'help') { + printHelp(); + return; + } + + if (command === 'v') { + console.log(`当前版本: v${version}`); + return; + } + + if (command === 'push' && argument === 'java') { + await require('../src/push')({ environment }); + return; + } + + console.error('无效命令,请执行 zy-gbs-cmd help 查看用法。'); + process.exitCode = 1; +} + +main().catch(error => { + console.error(`推送失败: ${error.message}`); + process.exitCode = 1; +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3faf176 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "zy-gbs-cmd", + "version": "1.0.0", + "private": false, + "description": "", + "author": "LiuJiaNan", + "license": "MIT", + "type": "commonjs", + "files": [ + "bin", + "src", + "README.md" + ], + "bin": { + "zy-gbs-cmd": "bin/command.js" + }, + "scripts": { + "test": "node test/package-manager.test.js && node test/push.test.js && node bin/command.js help", + "prepublishOnly": "yarn test", + "patch": "npm version patch", + "minor": "npm version minor", + "release": "npm publish" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/package-manager.js b/src/package-manager.js new file mode 100644 index 0000000..25f8209 --- /dev/null +++ b/src/package-manager.js @@ -0,0 +1,38 @@ +const childProcess = require('child_process'); + +function getExecutable(command) { + return process.platform === 'win32' ? `${command}.cmd` : command; +} + +function commandExists(command) { + const lookupCommand = process.platform === 'win32' ? 'where.exe' : 'which'; + const result = childProcess.spawnSync(lookupCommand, [getExecutable(command)], { + stdio: 'ignore', + }); + return result.status === 0; +} + +function ensureYarnAvailable(isCommandAvailable = commandExists) { + if (!isCommandAvailable('yarn')) { + throw new Error('未检测到 Yarn,请先安装 Yarn'); + } +} + +function runYarn(args, cwd) { + ensureYarnAvailable(); + const executable = getExecutable('yarn'); + console.log(`执行: yarn ${args.join(' ')}`); + if (process.platform === 'win32') { + childProcess.execFileSync(process.env.ComSpec || 'cmd.exe', [ + '/d', + '/s', + '/c', + executable, + ...args, + ], { cwd, stdio: 'inherit' }); + return; + } + childProcess.execFileSync(executable, args, { cwd, stdio: 'inherit' }); +} + +module.exports = { commandExists, ensureYarnAvailable, runYarn }; diff --git a/src/push.js b/src/push.js new file mode 100644 index 0000000..cf20be1 --- /dev/null +++ b/src/push.js @@ -0,0 +1,192 @@ +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'; +const JAVA_TEMPLATE_PATH = path.join('start', 'src', 'main', 'resources', 'templates'); + +function run(command, args, cwd) { + 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}`); + } + + delete require.cache[require.resolve(configPath)]; + const importedConfig = require(configPath); + 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) { + 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) { + const buildScript = `build:${environment}`; + if (!buildScripts.includes(buildScript)) { + throw new Error(`package.json 中没有构建脚本 ${buildScript}`); + } + return buildScript; +} + +function ensureDependencies(projectRoot) { + 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`)); +} + +async function pushJava({ environment: environmentArgument } = {}) { + const projectRoot = process.cwd(); + 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; + + if (environmentArgument) { + 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] || {} : {}; + const branch = environmentConfig.javaGitBranch || config.javaGitBranch || DEFAULT_BRANCH; + ensureYarnAvailable(); + + 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); + + ensureDependencies(projectRoot); + runYarn(['run', buildScript], projectRoot); + copyBuildOutput(projectRoot, javaRepository, config.appIdentifier); + + 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; + run('git', ['commit', '-m', message, '--no-verify'], javaRepository); + run('git', ['push'], javaRepository); + console.log('代码推送完成。'); + } finally { + input.close(); + if (temporaryRoot) { + try { + fs.rmSync(temporaryRoot, { recursive: true, force: true }); + } catch (error) { + console.warn(`临时目录清理失败: ${error.message}`); + } + } + } +} + +module.exports = pushJava; +module.exports.resolveBuildScript = resolveBuildScript; diff --git a/test/package-manager.test.js b/test/package-manager.test.js new file mode 100644 index 0000000..14fd26e --- /dev/null +++ b/test/package-manager.test.js @@ -0,0 +1,6 @@ +const assert = require('assert'); +const { commandExists, ensureYarnAvailable } = require('../src/package-manager'); + +assert.strictEqual(commandExists('yarn'), true, '当前环境未安装 Yarn'); +assert.doesNotThrow(() => ensureYarnAvailable(() => true)); +assert.throws(() => ensureYarnAvailable(() => false), /未检测到 Yarn/); diff --git a/test/push.test.js b/test/push.test.js new file mode 100644 index 0000000..fb39990 --- /dev/null +++ b/test/push.test.js @@ -0,0 +1,11 @@ +const assert = require('assert'); +const { resolveBuildScript } = require('../src/push'); + +const buildScripts = ['build', 'build:development', 'build:production']; + +assert.strictEqual(resolveBuildScript(buildScripts, 'production'), 'build:production'); +assert.strictEqual(resolveBuildScript(buildScripts, 'development'), 'build:development'); +assert.throws( + () => resolveBuildScript(buildScripts, 'test'), + /package.json 中没有构建脚本 build:test/ +);