import {spawn} from 'node:child_process'; import {existsSync, readFileSync} from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; const repoRoot = fileURLToPath(new URL('../', import.meta.url)); const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url)); const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url)); const caddyConfigPath = fileURLToPath(new URL('../tools/Caddyfile.dev', import.meta.url)); const distRoot = fileURLToPath(new URL('../dist/', import.meta.url)); const bundledCaddyExe = fileURLToPath(new URL('../tools/caddy.exe', import.meta.url)); function parseEnvContents(contents) { return contents .split(/\r?\n/u) .reduce((envMap, rawLine) => { const line = rawLine.trim(); if (!line || line.startsWith('#')) { return envMap; } const separatorIndex = line.indexOf('='); if (separatorIndex < 0) { return envMap; } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } envMap[key] = value; return envMap; }, {}); } function readEnvFile(filePath) { if (!existsSync(filePath)) { return {}; } return parseEnvContents(readFileSync(filePath, 'utf8')); } function normalizePathForCaddy(filePath) { return path.resolve(filePath).replace(/\\/gu, '/'); } function resolveApiUpstream(env) { return ( env.CADDY_API_UPSTREAM || env.NODE_SERVER_TARGET || 'http://127.0.0.1:8081' ); } function resolveCaddyBinary() { if (process.platform === 'win32' && existsSync(bundledCaddyExe)) { return bundledCaddyExe; } return process.platform === 'win32' ? 'caddy.exe' : 'caddy'; } const mergedEnv = { ...readEnvFile(envExamplePath), ...readEnvFile(envLocalPath), ...process.env, }; if (!existsSync(path.join(distRoot, 'index.html'))) { console.error('[serve:caddy] dist/index.html 不存在,请先运行 npm run build:raw'); process.exit(1); } mergedEnv.CADDY_SITE_ROOT = mergedEnv.CADDY_SITE_ROOT || normalizePathForCaddy(distRoot); mergedEnv.CADDY_API_UPSTREAM = resolveApiUpstream(mergedEnv); const caddyBinary = resolveCaddyBinary(); console.log('[serve:caddy] listen=:8080'); console.log(`[serve:caddy] CADDY_SITE_ROOT=${mergedEnv.CADDY_SITE_ROOT}`); console.log(`[serve:caddy] CADDY_API_UPSTREAM=${mergedEnv.CADDY_API_UPSTREAM}`); console.log(`[serve:caddy] config=${caddyConfigPath}`); const caddyProcess = spawn( caddyBinary, ['run', '--config', caddyConfigPath, '--adapter', 'caddyfile'], { cwd: repoRoot, env: mergedEnv, stdio: 'inherit', shell: process.platform === 'win32' && !existsSync(bundledCaddyExe), }, ); let shuttingDown = false; function requestShutdown(code = 0) { if (shuttingDown) { return; } shuttingDown = true; if (caddyProcess.exitCode === null) { caddyProcess.kill('SIGTERM'); setTimeout(() => { if (caddyProcess.exitCode === null) { caddyProcess.kill('SIGKILL'); } }, 2000).unref(); } if (caddyProcess.exitCode !== null) { process.exit(code); } } caddyProcess.on('error', (error) => { console.error('[serve:caddy] 启动 Caddy 失败', error); process.exit(1); }); caddyProcess.on('exit', (code, signal) => { if (!shuttingDown) { const resolvedExitCode = code ?? 1; const signalSuffix = signal ? ` (${signal})` : ''; console.error( `[serve:caddy] Caddy exited with code ${resolvedExitCode}${signalSuffix}`, ); process.exit(resolvedExitCode); } }); process.on('SIGINT', () => { console.log('[serve:caddy] received SIGINT, shutting down...'); requestShutdown(0); }); process.on('SIGTERM', () => { console.log('[serve:caddy] received SIGTERM, shutting down...'); requestShutdown(0); });