#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); const rootDir = path.join(__dirname, '..'); const buildDir = path.join(rootDir, 'build'); const distDir = path.join(rootDir, 'dist'); const bundleDir = path.join(buildDir, 'ncc'); const pkgProjectDir = path.join(buildDir, 'pkg'); const runtimeAssets = [ 'config.json', 'alModel.json', ]; const distDocs = [ '配置说明.md', '实施部署文档.md', ]; const targets = { 'win-x64': { pkgTarget: 'node18-win-x64', outputName: 'jh2028-service.exe', executableMode: null, }, 'linux-x64': { pkgTarget: 'node18-linux-x64', outputName: 'jh2028-service', executableMode: 0o755, }, }; function parseArgs(argv) { const selectedTargets = new Set(Object.keys(targets)); for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; const value = argv[index + 1]; if (arg === '--target' && value) { selectedTargets.clear(); for (const item of value.split(',').map((entry) => entry.trim()).filter(Boolean)) { if (!targets[item]) { throw new Error(`不支持的打包目标: ${item}`); } selectedTargets.add(item); } index += 1; } } return Array.from(selectedTargets); } function runNode(scriptPath, args) { const result = spawnSync(process.execPath, [scriptPath, ...args], { cwd: rootDir, stdio: 'inherit', }); if (result.status !== 0) { throw new Error(`命令执行失败: node ${scriptPath} ${args.join(' ')}`); } } function ensureCleanWorkspace() { fs.rmSync(buildDir, { recursive: true, force: true }); fs.rmSync(distDir, { recursive: true, force: true }); fs.mkdirSync(buildDir, { recursive: true }); fs.mkdirSync(distDir, { recursive: true }); } function buildBundle() { const nccCli = require.resolve('@vercel/ncc/dist/ncc/cli'); runNode(nccCli, ['build', 'app.js', '-o', bundleDir, '--target', 'es2019']); fs.rmSync(path.join(bundleDir, 'logs'), { recursive: true, force: true }); } function preparePkgProject() { const pkgBundleDir = path.join(pkgProjectDir, 'bundle'); const pkgEntryPath = path.join(pkgProjectDir, 'entry.js'); const pkgConfigPath = path.join(pkgProjectDir, 'package.json'); const bundleSourcePath = path.join(bundleDir, 'index.js'); const bundleTargetPath = path.join(pkgBundleDir, 'index.cjs'); const bootstrapSource = `#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const Module = require('module'); const bundlePath = path.join(__dirname, 'bundle', 'index.cjs'); const source = fs.readFileSync(bundlePath, 'utf8'); const bundledModule = new Module(bundlePath); bundledModule.filename = bundlePath; bundledModule.id = '.'; bundledModule.paths = Module._nodeModulePaths(path.dirname(bundlePath)); Module._cache[bundlePath] = bundledModule; process.mainModule = bundledModule; require.main = bundledModule; bundledModule._compile(source, bundlePath); `; const pkgConfig = { name: 'jh2028-pkg-bootstrap', version: '1.0.0', private: true, type: 'commonjs', bin: 'entry.js', pkg: { assets: [ 'bundle/index.cjs', ], }, }; fs.mkdirSync(pkgBundleDir, { recursive: true }); fs.copyFileSync(bundleSourcePath, bundleTargetPath); fs.writeFileSync(pkgEntryPath, bootstrapSource, 'utf8'); fs.writeFileSync(pkgConfigPath, `${JSON.stringify(pkgConfig, null, 2)}\n`, 'utf8'); } function buildExecutable(targetKey, targetConfig) { const pkgCli = require.resolve('pkg/lib-es5/bin.js'); const targetDir = path.join(distDir, targetKey); const outputPath = path.join(targetDir, targetConfig.outputName); fs.mkdirSync(targetDir, { recursive: true }); runNode(pkgCli, [ pkgProjectDir, '--targets', targetConfig.pkgTarget, '--output', outputPath, '--public', '--public-packages', '*', '--no-bytecode', ]); if (!fs.existsSync(outputPath)) { throw new Error(`未生成预期的可执行文件: ${outputPath}`); } if (targetConfig.executableMode) { fs.chmodSync(outputPath, targetConfig.executableMode); } } function copyRuntimeAssets(targetDir) { const runtimeDir = path.join(targetDir, 'runtime'); const logsDir = path.join(targetDir, 'logs'); const dashboardSourceDir = path.join(rootDir, 'dashboard'); const dashboardTargetDir = path.join(runtimeDir, 'dashboard'); fs.mkdirSync(runtimeDir, { recursive: true }); fs.mkdirSync(logsDir, { recursive: true }); for (const assetName of runtimeAssets) { const sourcePath = path.join(rootDir, assetName); if (!fs.existsSync(sourcePath)) { continue; } fs.copyFileSync(sourcePath, path.join(runtimeDir, assetName)); } if (fs.existsSync(dashboardSourceDir)) { fs.cpSync(dashboardSourceDir, dashboardTargetDir, { recursive: true }); } for (const docName of distDocs) { const sourcePath = path.join(rootDir, docName); if (!fs.existsSync(sourcePath)) { continue; } fs.copyFileSync(sourcePath, path.join(targetDir, docName)); } } function main() { const selectedTargets = parseArgs(process.argv); ensureCleanWorkspace(); buildBundle(); preparePkgProject(); for (const targetKey of selectedTargets) { buildExecutable(targetKey, targets[targetKey]); copyRuntimeAssets(path.join(distDir, targetKey)); } console.log('[BUILD] 打包完成'); } main();