199 lines
4.9 KiB
JavaScript
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);
|
|
});
|