#!/usr/bin/env node /** * 打包脚本 * * 用法: * node scripts/build.mjs # 完整打包(Next.js + Electron) * node scripts/build.mjs --web # 仅打包 Web(Next.js standalone) * node scripts/build.mjs --electron # 仅打包 Electron 桌面应用 * node scripts/build.mjs --no-build # 跳过编译,直接用上次产物打包 * * 环境变量: * NO_BUILD=1 等同于 --no-build */ import { spawn } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); // ── CLI flags ────────────────────────────────────────────────────────────── const args = process.argv.slice(2); const skipBuild = args.includes('--no-build') || process.env.NO_BUILD === '1'; const webOnly = args.includes('--web'); const electronOnly = args.includes('--electron'); const buildBoth = !webOnly && !electronOnly; // ── Helpers ──────────────────────────────────────────────────────────────── function run(cmd, cmdArgs, opts = {}) { return new Promise((resolve, reject) => { console.log(`\n$ ${cmd} ${cmdArgs.join(' ')}`); const p = spawn(cmd, cmdArgs, { stdio: 'inherit', cwd: root, shell: true, env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'zh_CN.UTF-8' }, ...opts }); p.on('close', (code) => { if (code === 0) resolve(); else reject(new Error(`命令失败(exit ${code}): ${cmd} ${cmdArgs.join(' ')}`)); }); p.on('error', reject); }); } async function exists(p) { try { await fs.access(p); return true; } catch { return false; } } function log(msg) { console.log(`\n\x1b[36m▶ ${msg}\x1b[0m`); } function success(msg) { console.log(`\x1b[32m✔ ${msg}\x1b[0m`); } // ── Web (Next.js standalone) ─────────────────────────────────────────────── async function buildWeb() { log('构建 Next.js...'); await run('yarn', ['build']); const standaloneDir = path.join(root, '.next', 'standalone'); if (!(await exists(standaloneDir))) { throw new Error('未找到 .next/standalone,请确认 next.config.mjs 中已设置 output: "standalone"'); } const outDir = path.join(root, 'dist'); log('组装 Web 产物到 dist/...'); await fs.rm(outDir, { recursive: true, force: true }); await fs.mkdir(outDir, { recursive: true }); await fs.cp(standaloneDir, outDir, { recursive: true }); // 复制 .next/static 和 public const nextStatic = path.join(root, '.next', 'static'); if (await exists(nextStatic)) { await fs.cp(nextStatic, path.join(outDir, '.next', 'static'), { recursive: true }); } const publicDir = path.join(root, 'public'); if (await exists(publicDir)) { await fs.cp(publicDir, path.join(outDir, 'public'), { recursive: true }); } success('Web 构建完成 → dist/'); } // ── Electron ─────────────────────────────────────────────────────────────── async function buildElectron() { log('编译 Electron 主进程 TypeScript...'); await run('yarn', ['electron:compile']); log('运行 electron-builder 打包...'); await run('yarn', ['electron-builder']); success('Electron 打包完成 → dist/'); } // ── Main ─────────────────────────────────────────────────────────────────── async function main() { const startTime = Date.now(); console.log('\x1b[1m\x1b[35m'); console.log('╔══════════════════════════════╗'); console.log('║ 小说创作助手 打包工具 ║'); console.log('╚══════════════════════════════╝'); console.log('\x1b[0m'); if (skipBuild) { console.log('⚠️ 已跳过编译步骤(--no-build)'); } try { if (buildBoth || webOnly) { if (!skipBuild) { await buildWeb(); } else { log('跳过 Next.js 构建'); } } if (buildBoth || electronOnly) { if (!skipBuild) { await buildElectron(); } else { log('跳过编译,直接运行 electron-builder...'); await run('yarn', ['electron-builder']); } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`\n\x1b[1m\x1b[32m✅ 打包完成!耗时 ${elapsed}s\x1b[0m`); console.log('\x1b[33m输出目录:dist/\x1b[0m\n'); } catch (err) { console.error(`\n\x1b[31m❌ 打包失败:${err.message}\x1b[0m\n`); process.exit(1); } } main();