lobehub/scripts/devStartupSequence.mts
Rdmclin2 dd192eda3e
feat: bot support custom markdown render and context injection (#13294)
* feat: support  bot mardown format

* feat: support custom markdownRender and bot context inject

* feat: support custom PORT

* feat: telegram support html render

* feat: slack support markdown render

* chore: feishu and lark don't handle markdown for now
2026-03-26 16:52:35 +08:00

135 lines
3.7 KiB
TypeScript

import { type ChildProcess, spawn } from 'node:child_process';
import dotenv from 'dotenv';
import net from 'node:net';
dotenv.config();
const NEXT_HOST = 'localhost';
/**
* Resolve the Next.js dev port.
* Respects the PORT environment variable, falls back to 3010.
*/
const resolveNextPort = (): number => {
if (process.env.PORT) return Number(process.env.PORT);
return 3010;
};
const NEXT_PORT = resolveNextPort();
const NEXT_ROOT_URL = `http://${NEXT_HOST}:${NEXT_PORT}/`;
const NEXT_READY_TIMEOUT_MS = 180_000;
const NEXT_READY_RETRY_MS = 400;
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
let nextProcess: ChildProcess | undefined;
let viteProcess: ChildProcess | undefined;
let shuttingDown = false;
const runNpmScript = (scriptName: string) =>
spawn(npmCommand, ['run', scriptName], {
env: process.env,
stdio: 'inherit',
shell: process.platform === 'win32',
});
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const isPortOpen = (host: string, port: number) =>
new Promise<boolean>((resolve) => {
const socket = net.createConnection({ host, port });
const onDone = (result: boolean) => {
socket.removeAllListeners();
socket.destroy();
resolve(result);
};
socket.once('connect', () => onDone(true));
socket.once('error', () => onDone(false));
socket.setTimeout(1_000, () => onDone(false));
});
const waitForNextReady = async () => {
const startedAt = Date.now();
while (Date.now() - startedAt < NEXT_READY_TIMEOUT_MS) {
if (await isPortOpen(NEXT_HOST, NEXT_PORT)) return;
await wait(NEXT_READY_RETRY_MS);
}
throw new Error(
`Next server was not ready within ${NEXT_READY_TIMEOUT_MS / 1000}s on ${NEXT_HOST}:${NEXT_PORT}`,
);
};
const prewarmNextRootCompile = async () => {
const response = await fetch(NEXT_ROOT_URL, { signal: AbortSignal.timeout(120_000) });
console.log(`✅ Next prewarm request finished (${response.status}) ${NEXT_ROOT_URL}`);
};
const runNextBackgroundTasks = () => {
setTimeout(() => {
console.log(`🔁 Next server URL: ${NEXT_ROOT_URL}`);
}, 2_000);
void (async () => {
try {
await waitForNextReady();
await prewarmNextRootCompile();
} catch (error) {
console.warn('⚠️ Next prewarm skipped:', error);
}
})();
};
const terminateChild = (child?: ChildProcess) => {
if (!child || child.killed) return;
child.kill('SIGTERM');
};
const shutdownAll = (signal: NodeJS.Signals) => {
if (shuttingDown) return;
shuttingDown = true;
terminateChild(viteProcess);
terminateChild(nextProcess);
process.exitCode = signal === 'SIGINT' ? 130 : 143;
};
const watchChildExit = (child: ChildProcess, name: 'next' | 'vite') => {
child.once('exit', (code, signal) => {
if (!shuttingDown) {
console.error(
`${name} exited unexpectedly (code: ${code ?? 'null'}, signal: ${signal ?? 'null'})`,
);
shutdownAll('SIGTERM');
}
});
};
const main = async () => {
process.once('SIGINT', () => shutdownAll('SIGINT'));
process.once('SIGTERM', () => shutdownAll('SIGTERM'));
nextProcess = spawn('npx', ['next', 'dev', '-p', String(NEXT_PORT)], {
env: process.env,
stdio: 'inherit',
shell: process.platform === 'win32',
});
watchChildExit(nextProcess, 'next');
viteProcess = runNpmScript('dev:spa');
watchChildExit(viteProcess, 'vite');
runNextBackgroundTasks();
await Promise.race([
new Promise((resolve) => nextProcess?.once('exit', resolve)),
new Promise((resolve) => viteProcess?.once('exit', resolve)),
]);
};
void main().catch((error) => {
console.error('❌ dev startup sequence failed:', error);
shutdownAll('SIGTERM');
});