168 lines
4.1 KiB
JavaScript
168 lines
4.1 KiB
JavaScript
import {spawn} from 'node:child_process';
|
|
import {existsSync, readFileSync} from 'node:fs';
|
|
import {resolve} from 'node:path';
|
|
import {
|
|
findAvailablePort,
|
|
formatPortDecision,
|
|
normalizePort,
|
|
} from './dev-stack-port-utils.mjs';
|
|
|
|
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 buildTargetCandidates() {
|
|
const candidates = [
|
|
fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET,
|
|
fileEnv.RUST_SERVER_TARGET,
|
|
fileEnv.GENARRATIVE_API_TARGET,
|
|
`http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT || '3100'}`,
|
|
'http://127.0.0.1:8082',
|
|
'http://127.0.0.1:3100',
|
|
].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 candidates = buildTargetCandidates();
|
|
const reachableTargets = [];
|
|
|
|
for (const target of candidates) {
|
|
if (await isTargetReachable(target)) {
|
|
reachableTargets.push(target);
|
|
if (
|
|
target === fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET ||
|
|
target === fileEnv.RUST_SERVER_TARGET ||
|
|
target === fileEnv.GENARRATIVE_API_TARGET
|
|
) {
|
|
return {
|
|
target,
|
|
fallbackUsed: false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (reachableTargets.length > 0) {
|
|
return {
|
|
target: reachableTargets[0],
|
|
fallbackUsed: true,
|
|
};
|
|
}
|
|
|
|
return {
|
|
target:
|
|
fileEnv.GENARRATIVE_RUNTIME_SERVER_TARGET ||
|
|
fileEnv.RUST_SERVER_TARGET ||
|
|
fileEnv.GENARRATIVE_API_TARGET ||
|
|
`http://127.0.0.1:${fileEnv.GENARRATIVE_API_PORT || '3100'}`,
|
|
fallbackUsed: false,
|
|
};
|
|
}
|
|
|
|
const runtimeTarget = await resolveRuntimeTarget();
|
|
if (runtimeTarget.fallbackUsed) {
|
|
console.warn(
|
|
`[dev:web] 配置的 Rust target 不可用,已切换到 ${runtimeTarget.target}`,
|
|
);
|
|
}
|
|
|
|
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 webHost = '0.0.0.0';
|
|
const preferredWebPort = normalizePort(fileEnv.WEB_PORT, 3000);
|
|
const webPort = await findAvailablePort({
|
|
host: webHost,
|
|
preferredPort: preferredWebPort,
|
|
});
|
|
console.log(
|
|
formatPortDecision({
|
|
name: 'web',
|
|
host: webHost,
|
|
preferredPort: preferredWebPort,
|
|
resolvedPort: webPort,
|
|
}),
|
|
);
|
|
|
|
const child = spawn(
|
|
'node',
|
|
['scripts/vite-cli.mjs', `--port=${webPort}`, `--host=${webHost}`, '--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);
|
|
});
|