const fs = require('node:fs'); const path = require('node:path'); const cp = require('node:child_process'); const backendDir = path.resolve(__dirname, '..'); const repoDir = path.resolve(backendDir, '..', '..'); const frontendDir = path.join(repoDir, 'packages', 'frontend'); const stageDir = path.join(backendDir, 'build', 'pkg-stage'); const outputDir = path.join(backendDir, 'build', 'pkg-output'); function run(command, cwd, env) { cp.execFileSync( process.platform === 'win32' ? 'cmd.exe' : 'bash', process.platform === 'win32' ? ['/c', command] : ['-lc', command], { cwd, stdio: 'inherit', env: { ...process.env, ...env } } ); } function assertStage(dir) { const required = [ path.join(dir, 'public', 'index.html'), path.join(dir, 'dist', 'index.js'), ]; for (const file of required) { if (!fs.existsSync(file)) { throw new Error(`缺少 staging 文件: ${file}`); } } } // Clean fs.rmSync(stageDir, { recursive: true, force: true }); fs.rmSync(outputDir, { recursive: true, force: true }); fs.mkdirSync(stageDir, { recursive: true }); // Build frontend (override API base URL to use same-origin for installer) console.log('[1/6] Building frontend...'); run('pnpm build', frontendDir, { VITE_API_BASE_URL: '', VITE_COOL_EPS_ENABLE: 'true' }); // Build backend console.log('[2/6] Building backend...'); run('npm run build', backendDir); // Stage files console.log('[3/6] Staging files...'); fs.cpSync(path.join(backendDir, 'bootstrap.js'), path.join(stageDir, 'bootstrap.js')); fs.cpSync(path.join(backendDir, 'dist'), path.join(stageDir, 'dist'), { recursive: true }); fs.cpSync(path.join(backendDir, 'public'), path.join(stageDir, 'public'), { recursive: true }); // Overlay frontend dist onto public (frontend index.html replaces backend welcome page) fs.cpSync(path.join(frontendDir, 'dist'), path.join(stageDir, 'public'), { recursive: true, force: true }); if (fs.existsSync(path.join(backendDir, 'typings'))) { fs.cpSync(path.join(backendDir, 'typings'), path.join(stageDir, 'typings'), { recursive: true }); } if (fs.existsSync(path.join(backendDir, 'src', 'locales'))) { fs.mkdirSync(path.join(stageDir, 'src'), { recursive: true }); fs.cpSync(path.join(backendDir, 'src', 'locales'), path.join(stageDir, 'src', 'locales'), { recursive: true }); } // Generate staging package.json console.log('[4/6] Generating staging package.json...'); const backendPkg = require(path.join(backendDir, 'package.json')); const stagePkg = { name: backendPkg.name, version: backendPkg.version, private: true, overrides: backendPkg.overrides || {}, dependencies: backendPkg.dependencies, bin: './bootstrap.js', pkg: { scripts: ['dist/**/*.js', 'node_modules/**/*.mjs'], assets: [ 'public/**/*', 'typings/**/*', 'src/locales/**/*', 'node_modules/@img/sharp-win32-x64/**/*', 'node_modules/@img/colour/**/*', 'node_modules/better-sqlite3/build/Release/*.node', 'node_modules/@msgpackr-extract/msgpackr-extract-win32-x64/*.node', 'node_modules/@napi-rs/canvas-win32-x64-msvc/*.node', ], targets: ['node20-win-x64'], outputPath: outputDir, }, }; fs.writeFileSync(path.join(stageDir, 'package.json'), JSON.stringify(stagePkg, null, 2)); // Install deps console.log('[5/6] Installing production dependencies...'); run('npm install --production --install-strategy=hoisted --legacy-peer-deps', stageDir); // Patch generator-function const gfPkg = path.join(stageDir, 'node_modules', 'generator-function', 'package.json'); if (fs.existsSync(gfPkg)) { const gf = JSON.parse(fs.readFileSync(gfPkg, 'utf8')); if (gf.exports && gf.exports['.']) { gf.exports['.'] = './index.js'; fs.writeFileSync(gfPkg, JSON.stringify(gf, null, 2)); console.log(' Patched generator-function exports'); } } // Run pkg console.log('[6/6] Running pkg...'); assertStage(stageDir); fs.mkdirSync(outputDir, { recursive: true }); run('npx @yao-pkg/pkg@6.14.1 .', stageDir); // Validate console.log('\n=== Build complete ==='); const exePath = path.join(outputDir, 'backend.exe'); if (fs.existsSync(exePath)) { const stat = fs.statSync(exePath); console.log(`Output: ${exePath} (${(stat.size / 1024 / 1024).toFixed(1)} MB)`); }