feat: migrate runtime backend to node server
This commit is contained in:
198
scripts/dev-node.mjs
Normal file
198
scripts/dev-node.mjs
Normal file
@@ -0,0 +1,198 @@
|
||||
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);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
149
scripts/run-caddy-dev.mjs
Normal file
149
scripts/run-caddy-dev.mjs
Normal file
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user