build.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env node
  2. /**
  3. * 打包脚本
  4. *
  5. * 用法:
  6. * node scripts/build.mjs # 完整打包(Next.js + Electron)
  7. * node scripts/build.mjs --web # 仅打包 Web(Next.js standalone)
  8. * node scripts/build.mjs --electron # 仅打包 Electron 桌面应用
  9. * node scripts/build.mjs --no-build # 跳过编译,直接用上次产物打包
  10. *
  11. * 环境变量:
  12. * NO_BUILD=1 等同于 --no-build
  13. */
  14. import { spawn } from 'child_process';
  15. import fs from 'fs/promises';
  16. import path from 'path';
  17. import { fileURLToPath } from 'url';
  18. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  19. const root = path.resolve(__dirname, '..');
  20. // ── CLI flags ──────────────────────────────────────────────────────────────
  21. const args = process.argv.slice(2);
  22. const skipBuild = args.includes('--no-build') || process.env.NO_BUILD === '1';
  23. const webOnly = args.includes('--web');
  24. const electronOnly = args.includes('--electron');
  25. const buildBoth = !webOnly && !electronOnly;
  26. // ── Helpers ────────────────────────────────────────────────────────────────
  27. function run(cmd, cmdArgs, opts = {}) {
  28. return new Promise((resolve, reject) => {
  29. console.log(`\n$ ${cmd} ${cmdArgs.join(' ')}`);
  30. const p = spawn(cmd, cmdArgs, {
  31. stdio: 'inherit',
  32. cwd: root,
  33. shell: true,
  34. env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'zh_CN.UTF-8' },
  35. ...opts
  36. });
  37. p.on('close', (code) => {
  38. if (code === 0) resolve();
  39. else reject(new Error(`命令失败(exit ${code}): ${cmd} ${cmdArgs.join(' ')}`));
  40. });
  41. p.on('error', reject);
  42. });
  43. }
  44. async function exists(p) {
  45. try { await fs.access(p); return true; } catch { return false; }
  46. }
  47. function log(msg) {
  48. console.log(`\n\x1b[36m▶ ${msg}\x1b[0m`);
  49. }
  50. function success(msg) {
  51. console.log(`\x1b[32m✔ ${msg}\x1b[0m`);
  52. }
  53. // ── Web (Next.js standalone) ───────────────────────────────────────────────
  54. async function buildWeb() {
  55. log('构建 Next.js...');
  56. await run('yarn', ['build']);
  57. const standaloneDir = path.join(root, '.next', 'standalone');
  58. if (!(await exists(standaloneDir))) {
  59. throw new Error('未找到 .next/standalone,请确认 next.config.mjs 中已设置 output: "standalone"');
  60. }
  61. const outDir = path.join(root, 'dist');
  62. log('组装 Web 产物到 dist/...');
  63. await fs.rm(outDir, { recursive: true, force: true });
  64. await fs.mkdir(outDir, { recursive: true });
  65. await fs.cp(standaloneDir, outDir, { recursive: true });
  66. // 复制 .next/static 和 public
  67. const nextStatic = path.join(root, '.next', 'static');
  68. if (await exists(nextStatic)) {
  69. await fs.cp(nextStatic, path.join(outDir, '.next', 'static'), { recursive: true });
  70. }
  71. const publicDir = path.join(root, 'public');
  72. if (await exists(publicDir)) {
  73. await fs.cp(publicDir, path.join(outDir, 'public'), { recursive: true });
  74. }
  75. success('Web 构建完成 → dist/');
  76. }
  77. // ── Electron ───────────────────────────────────────────────────────────────
  78. async function buildElectron() {
  79. log('编译 Electron 主进程 TypeScript...');
  80. await run('yarn', ['electron:compile']);
  81. log('运行 electron-builder 打包...');
  82. await run('yarn', ['electron-builder']);
  83. success('Electron 打包完成 → dist/');
  84. }
  85. // ── Main ───────────────────────────────────────────────────────────────────
  86. async function main() {
  87. const startTime = Date.now();
  88. console.log('\x1b[1m\x1b[35m');
  89. console.log('╔══════════════════════════════╗');
  90. console.log('║ 小说创作助手 打包工具 ║');
  91. console.log('╚══════════════════════════════╝');
  92. console.log('\x1b[0m');
  93. if (skipBuild) {
  94. console.log('⚠️ 已跳过编译步骤(--no-build)');
  95. }
  96. try {
  97. if (buildBoth || webOnly) {
  98. if (!skipBuild) {
  99. await buildWeb();
  100. } else {
  101. log('跳过 Next.js 构建');
  102. }
  103. }
  104. if (buildBoth || electronOnly) {
  105. if (!skipBuild) {
  106. await buildElectron();
  107. } else {
  108. log('跳过编译,直接运行 electron-builder...');
  109. await run('yarn', ['electron-builder']);
  110. }
  111. }
  112. const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
  113. console.log(`\n\x1b[1m\x1b[32m✅ 打包完成!耗时 ${elapsed}s\x1b[0m`);
  114. console.log('\x1b[33m输出目录:dist/\x1b[0m\n');
  115. } catch (err) {
  116. console.error(`\n\x1b[31m❌ 打包失败:${err.message}\x1b[0m\n`);
  117. process.exit(1);
  118. }
  119. }
  120. main();