Files
Genarrative/scripts/dev-node.mjs
2026-04-08 16:41:29 +08:00

199 lines
4.9 KiB
JavaScript

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 serverRoot = fileURLToPath(new URL('../server-node/', import.meta.url));
const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url));
const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url));
const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url));
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
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 resolveServerTarget(serverAddr) {
const trimmed = serverAddr.trim();
if (!trimmed) {
return 'http://127.0.0.1:8081';
}
if (/^https?:\/\//u.test(trimmed)) {
try {
const url = new URL(trimmed);
if (url.hostname === '0.0.0.0') {
url.hostname = '127.0.0.1';
}
return url.toString().replace(/\/$/u, '');
} catch {
return trimmed.replace(/\/$/u, '');
}
}
if (trimmed.startsWith(':')) {
return `http://127.0.0.1${trimmed}`;
}
if (trimmed.startsWith('0.0.0.0:')) {
return `http://127.0.0.1:${trimmed.slice('0.0.0.0:'.length)}`;
}
return `http://${trimmed}`;
}
const mergedEnv = {
...readEnvFile(envExamplePath),
...readEnvFile(envLocalPath),
...process.env,
};
mergedEnv.PROJECT_ROOT = mergedEnv.PROJECT_ROOT || repoRoot;
mergedEnv.NODE_SERVER_ADDR = mergedEnv.NODE_SERVER_ADDR || ':8081';
mergedEnv.NODE_SERVER_TARGET =
mergedEnv.NODE_SERVER_TARGET || resolveServerTarget(mergedEnv.NODE_SERVER_ADDR);
mergedEnv.SQLITE_PATH =
mergedEnv.SQLITE_PATH || path.join(repoRoot, 'server-node', 'data', 'genarrative.sqlite');
console.log(`[dev:node] PROJECT_ROOT=${mergedEnv.PROJECT_ROOT}`);
console.log(`[dev:node] NODE_SERVER_ADDR=${mergedEnv.NODE_SERVER_ADDR}`);
console.log(`[dev:node] NODE_SERVER_TARGET=${mergedEnv.NODE_SERVER_TARGET}`);
console.log(`[dev:node] SQLITE_PATH=${mergedEnv.SQLITE_PATH}`);
const children = new Set();
let shuttingDown = false;
let pendingExitCode = 0;
function stopChild(child) {
if (!child || child.exitCode !== null) {
return;
}
child.kill('SIGTERM');
setTimeout(() => {
if (child.exitCode === null) {
child.kill('SIGKILL');
}
}, 2000).unref();
}
function stopAllChildren() {
for (const child of children) {
stopChild(child);
}
}
function finalizeExit(code = 0) {
pendingExitCode = code;
if (children.size === 0) {
process.exit(pendingExitCode);
}
}
function requestShutdown(code = 0) {
if (!shuttingDown) {
shuttingDown = true;
pendingExitCode = code;
stopAllChildren();
}
finalizeExit(pendingExitCode);
}
function registerChild(name, child, siblingProvider) {
children.add(child);
child.on('error', (error) => {
console.error(`[dev:node] ${name} failed to start`, error);
requestShutdown(1);
});
child.on('exit', (code, signal) => {
children.delete(child);
if (!shuttingDown) {
const resolvedExitCode = code ?? 1;
const signalSuffix = signal ? ` (${signal})` : '';
console.error(
`[dev:node] ${name} exited with code ${resolvedExitCode}${signalSuffix}`,
);
const sibling = siblingProvider();
if (sibling) {
stopChild(sibling);
}
requestShutdown(resolvedExitCode);
return;
}
finalizeExit(pendingExitCode);
});
}
const serverProcess = spawn(npmCommand, ['run', 'dev'], {
cwd: serverRoot,
env: mergedEnv,
shell: process.platform === 'win32',
stdio: 'inherit',
});
const viteProcess = spawn(
process.execPath,
[viteCliPath, '--port=3000', '--host=0.0.0.0'],
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
},
);
registerChild('node server', serverProcess, () => viteProcess);
registerChild('vite dev server', viteProcess, () => serverProcess);
process.on('SIGINT', () => {
console.log('[dev:node] received SIGINT, shutting down...');
requestShutdown(0);
});
process.on('SIGTERM', () => {
console.log('[dev:node] received SIGTERM, shutting down...');
requestShutdown(0);
});