import { spawn } from 'node:child_process'; import { createRequire } from 'node:module'; import { createInterface } from 'node:readline'; const require = createRequire(import.meta.url); const child = spawn( process.execPath, [require.resolve('@nestjs/cli/bin/nest.js'), 'start', '--watch'], { env: { ...process.env, DOODLE_DEV_LAUNCHER: '1' }, stdio: ['inherit', 'pipe', 'inherit'], }, ); let opened = false; /** 使用系统默认浏览器,不经 shell 拼接 URL;打开失败不影响开发服务继续运行。 */ function openBrowser(url) { const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer.exe' : 'xdg-open'; const browser = spawn(command, [url], { stdio: 'ignore', detached: true }); const warn = () => process.stderr.write(`无法自动打开浏览器,请手动访问:${url}\n`); browser.once('error', warn); browser.once('exit', (code) => { if (code !== 0 && code !== null) warn(); }); browser.unref(); } // 只在本次启动器收到子服务的成功通知后打开;热更新重启不会反复弹出页面。 const lines = createInterface({ input: child.stdout }); lines.on('line', (line) => { let event; try { event = JSON.parse(line); } catch { /* Nest 编译提示不是 JSON,原样转发。 */ } if (event?.event !== 'doodle-dev-ready') { process.stdout.write(`${line}\n`); return; } if (!event.swaggerUrl) { process.stdout.write('\nSwagger 已关闭(SWAGGER_ENABLED=false)。\n\n'); return; } process.stdout.write(`\nSwagger: ${event.swaggerUrl}\n\n`); if (!opened) { opened = true; openBrowser(event.swaggerUrl); } }); // 退出启动器时将信号传给 Nest,保留其对子服务的正常关闭和端口释放流程。 for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal)); child.once('error', () => { process.stderr.write('开发服务启动失败,请检查本地 Node.js 与项目依赖。\n'); process.exitCode = 1; }); child.once('exit', (code, signal) => { lines.close(); process.exitCode = code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1); });