1
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const apiServerExePath = resolve(
|
||||
@@ -8,8 +9,9 @@ const apiServerExePath = resolve(
|
||||
'server-rs/target/debug/api-server.exe',
|
||||
);
|
||||
const shellEnvKeys = new Set(Object.keys(process.env));
|
||||
const LOCAL_ENV_FILES = ['.env', '.env.local', '.env.secrets.local'];
|
||||
|
||||
function loadEnvFile(path, target) {
|
||||
function loadEnvFile(path, target, protectedKeys = shellEnvKeys) {
|
||||
if (!existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
@@ -28,8 +30,9 @@ function loadEnvFile(path, target) {
|
||||
|
||||
const [, key, rawValue] = match;
|
||||
// 只保留启动命令行和外层 shell 已显式传入的环境变量优先级;
|
||||
// `.env.local` 需要能覆盖 `.env`,否则本地短信登录会被默认 false 压住。
|
||||
if (shellEnvKeys.has(key)) {
|
||||
// `.env.local` 与 `.env.secrets.local` 需要能覆盖 `.env`,
|
||||
// 否则本地短信登录或私密模型密钥会被默认空值压住。
|
||||
if (protectedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -37,23 +40,20 @@ function loadEnvFile(path, target) {
|
||||
}
|
||||
}
|
||||
|
||||
const mergedEnv = { ...process.env };
|
||||
loadEnvFile(resolve(repoRoot, '.env.secrets.local'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.local'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env'), mergedEnv);
|
||||
export function loadApiServerEnv(
|
||||
repoRootPath,
|
||||
target,
|
||||
protectedKeys = shellEnvKeys,
|
||||
) {
|
||||
for (const fileName of LOCAL_ENV_FILES) {
|
||||
loadEnvFile(resolve(repoRootPath, fileName), target, protectedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
mergedEnv.GENARRATIVE_API_HOST = mergedEnv.GENARRATIVE_API_HOST || '127.0.0.1';
|
||||
mergedEnv.GENARRATIVE_API_PORT = mergedEnv.GENARRATIVE_API_PORT || '3100';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || 'http://127.0.0.1:3101';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE = mergedEnv.GENARRATIVE_SPACETIME_DATABASE || '';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN = mergedEnv.GENARRATIVE_SPACETIME_TOKEN || '';
|
||||
|
||||
if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) {
|
||||
console.error(
|
||||
'[api-server] 缺少 GENARRATIVE_SPACETIME_DATABASE。',
|
||||
);
|
||||
process.exit(1);
|
||||
export function mergeApiServerEnv(repoRootPath, baseEnv = process.env) {
|
||||
const mergedEnv = { ...baseEnv };
|
||||
loadApiServerEnv(repoRootPath, mergedEnv, new Set(Object.keys(baseEnv)));
|
||||
return mergedEnv;
|
||||
}
|
||||
|
||||
function stopExistingWindowsApiServer() {
|
||||
@@ -97,39 +97,63 @@ function stopExistingWindowsApiServer() {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
stopExistingWindowsApiServer();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[api-server] 清理旧 api-server 进程失败: ${error.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
function main() {
|
||||
const mergedEnv = mergeApiServerEnv(repoRoot);
|
||||
|
||||
console.log(
|
||||
`[api-server] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
|
||||
);
|
||||
mergedEnv.GENARRATIVE_API_HOST =
|
||||
mergedEnv.GENARRATIVE_API_HOST || '127.0.0.1';
|
||||
mergedEnv.GENARRATIVE_API_PORT = mergedEnv.GENARRATIVE_API_PORT || '3100';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || 'http://127.0.0.1:3101';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE || '';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN || '';
|
||||
|
||||
const child = spawn(
|
||||
'cargo',
|
||||
['run', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`[api-server] 启动 cargo 失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
console.error(`[api-server] api-server 被信号终止: ${signal}`);
|
||||
if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) {
|
||||
console.error('[api-server] 缺少 GENARRATIVE_SPACETIME_DATABASE。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
try {
|
||||
stopExistingWindowsApiServer();
|
||||
} catch (error) {
|
||||
console.error(`[api-server] 清理旧 api-server 进程失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[api-server] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
|
||||
);
|
||||
|
||||
const child = spawn(
|
||||
'cargo',
|
||||
['run', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (error) => {
|
||||
console.error(`[api-server] 启动 cargo 失败: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
console.error(`[api-server] api-server 被信号终止: ${signal}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
main();
|
||||
}
|
||||
|
||||
66
scripts/api-server-dev.test.ts
Normal file
66
scripts/api-server-dev.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { mergeApiServerEnv } from './api-server-dev.mjs';
|
||||
|
||||
type EnvMap = Record<string, string>;
|
||||
|
||||
function withTempEnvFiles(
|
||||
files: Record<string, string>,
|
||||
assertEnv: (env: EnvMap, tempDir: string) => void,
|
||||
) {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-api-env-'));
|
||||
|
||||
try {
|
||||
for (const [fileName, content] of Object.entries(files)) {
|
||||
writeFileSync(join(tempDir, fileName), content, 'utf8');
|
||||
}
|
||||
|
||||
assertEnv(mergeApiServerEnv(tempDir, {}) as EnvMap, tempDir);
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
describe('api-server-dev env merge', () => {
|
||||
test('.env.local 和 .env.secrets.local 可以覆盖 .env 默认值', () => {
|
||||
withTempEnvFiles(
|
||||
{
|
||||
'.env': [
|
||||
'SMS_AUTH_ENABLED=false',
|
||||
'HYPER3D_API_KEY=',
|
||||
'GENARRATIVE_SPACETIME_DATABASE=from-env',
|
||||
].join('\n'),
|
||||
'.env.local': [
|
||||
'SMS_AUTH_ENABLED=true',
|
||||
'HYPER3D_API_KEY=local-key',
|
||||
].join('\n'),
|
||||
'.env.secrets.local': 'HYPER3D_API_KEY=secret-key',
|
||||
},
|
||||
(env) => {
|
||||
expect(env.SMS_AUTH_ENABLED).toBe('true');
|
||||
expect(env.HYPER3D_API_KEY).toBe('secret-key');
|
||||
expect(env.GENARRATIVE_SPACETIME_DATABASE).toBe('from-env');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('外层 shell 变量优先于本地 env 文件', () => {
|
||||
withTempEnvFiles(
|
||||
{
|
||||
'.env': 'HYPER3D_API_KEY=from-env',
|
||||
'.env.local': 'HYPER3D_API_KEY=from-local',
|
||||
'.env.secrets.local': 'HYPER3D_API_KEY=from-secrets',
|
||||
},
|
||||
(_env, tempDir) => {
|
||||
expect(
|
||||
mergeApiServerEnv(tempDir, { HYPER3D_API_KEY: 'from-shell' })
|
||||
.HYPER3D_API_KEY,
|
||||
).toBe('from-shell');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user