Files
Genarrative/scripts/dev-web-rust.mjs
2026-05-09 17:15:23 +08:00

161 lines
3.8 KiB
JavaScript

import {spawn} from 'node:child_process';
import {existsSync, readFileSync} from 'node:fs';
import {resolve} from 'node:path';
const repoRoot = process.cwd();
const shellEnvKeys = new Set(Object.keys(process.env));
function loadEnvFile(path, target) {
if (!existsSync(path)) {
return;
}
const rawText = readFileSync(path, 'utf8');
for (const rawLine of rawText.split(/\r?\n/u)) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
continue;
}
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
if (!match) {
continue;
}
const [, key, rawValue] = match;
// 中文注释:命令行显式传入的目标优先,`.env.local` 再覆盖 `.env`,与 api-server 启动脚本保持一致。
if (shellEnvKeys.has(key)) {
continue;
}
target[key] = rawValue.replace(/^['"]|['"]$/gu, '');
}
}
const fileEnv = {...process.env};
loadEnvFile(resolve(repoRoot, '.env'), fileEnv);
loadEnvFile(resolve(repoRoot, '.env.local'), fileEnv);
loadEnvFile(resolve(repoRoot, '.env.secrets.local'), fileEnv);
function resolveConfiguredTarget() {
if (fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET) {
return fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET;
}
if (fileEnv.RUST_SERVER_TARGET) {
return fileEnv.RUST_SERVER_TARGET;
}
if (fileEnv.GENARRATIVE_API_TARGET) {
return fileEnv.GENARRATIVE_API_TARGET;
}
if (fileEnv.GENARRATIVE_API_PORT) {
return `http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT}`;
}
return '';
}
function buildFallbackCandidates() {
const candidates = [
'http://127.0.0.1:3100',
'http://127.0.0.1:8082',
].filter(Boolean);
return Array.from(new Set(candidates));
}
async function isTargetReachable(target) {
const healthUrl = new URL('/healthz', target);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1200);
try {
const response = await fetch(healthUrl, {
method: 'GET',
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timeoutId);
}
}
async function resolveRuntimeTarget() {
const configuredTarget = resolveConfiguredTarget();
if (configuredTarget) {
return {
target: configuredTarget,
fallbackUsed: false,
targetUnavailable: !(await isTargetReachable(configuredTarget)),
};
}
for (const target of buildFallbackCandidates()) {
if (await isTargetReachable(target)) {
return {
target,
fallbackUsed: true,
targetUnavailable: false,
};
}
}
return {
target: 'http://127.0.0.1:3100',
fallbackUsed: false,
targetUnavailable: true,
};
}
const runtimeTarget = await resolveRuntimeTarget();
if (runtimeTarget.fallbackUsed) {
console.warn(
`[dev:web] 配置的 Rust target 不可用,已切换到 ${runtimeTarget.target}`,
);
}
if (runtimeTarget.targetUnavailable) {
console.warn(
`[dev:web] Rust target 当前不可用: ${runtimeTarget.target},请先启动 api-server。`,
);
}
const mergedEnv = {
...fileEnv,
RUST_SERVER_TARGET: runtimeTarget.target,
GENARRATIVE_RUNTIME_SERVER_TARGET: runtimeTarget.target,
};
console.log(`[dev:web] backend=rust target=${mergedEnv.GENARRATIVE_RUNTIME_SERVER_TARGET}`);
const child = spawn(
'node',
['scripts/vite-cli.mjs', '--port=3000', '--host=0.0.0.0', '--strictPort'],
{
cwd: process.cwd(),
env: mergedEnv,
stdio: 'inherit',
shell: process.platform === 'win32',
},
);
child.on('error', (error) => {
console.error(`[dev:web] 启动 Vite 失败: ${error.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
console.error(`[dev:web] Vite 被信号终止: ${signal}`);
process.exit(1);
}
process.exit(code ?? 0);
});