fix(tooling): harden database env loading

This commit is contained in:
2026-03-30 14:42:44 +02:00
parent be6be64e3d
commit 34067f1576
6 changed files with 179 additions and 35 deletions
@@ -0,0 +1,85 @@
import { afterEach, describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadWorkspaceEnv, resolveWorkspaceEnvPaths } from "./load-workspace-env.js";
const envKeys = [
"DATABASE_URL",
"SHARED_VALUE",
"LOCAL_ONLY",
"MODE_ONLY",
"MODE_LOCAL_ONLY",
];
function clearEnv() {
for (const key of envKeys) {
delete process.env[key];
}
}
function withTempWorkspace(run: (workspaceRoot: string) => void) {
const workspaceRoot = mkdtempSync(join(tmpdir(), "capakraken-env-"));
try {
run(workspaceRoot);
} finally {
rmSync(workspaceRoot, { recursive: true, force: true });
}
}
afterEach(() => {
clearEnv();
delete process.env.NODE_ENV;
});
describe("loadWorkspaceEnv", () => {
it("loads standard workspace env files in precedence order", () => {
withTempWorkspace((workspaceRoot) => {
writeFileSync(join(workspaceRoot, ".env"), "DATABASE_URL=postgres://from-env\nSHARED_VALUE=base\n");
writeFileSync(join(workspaceRoot, ".env.development"), "SHARED_VALUE=mode\nMODE_ONLY=development\n");
writeFileSync(join(workspaceRoot, ".env.local"), "SHARED_VALUE=local\nLOCAL_ONLY=1\n");
writeFileSync(join(workspaceRoot, ".env.development.local"), "SHARED_VALUE=mode-local\nMODE_LOCAL_ONLY=1\n");
process.env.NODE_ENV = "development";
const loadedPaths = loadWorkspaceEnv({ workspaceRoot });
assert.deepEqual(
loadedPaths.map((entry) => entry.slice(workspaceRoot.length + 1)),
[".env", ".env.development", ".env.local", ".env.development.local"],
);
assert.equal(process.env.DATABASE_URL, "postgres://from-env");
assert.equal(process.env.SHARED_VALUE, "mode-local");
assert.equal(process.env.LOCAL_ONLY, "1");
assert.equal(process.env.MODE_ONLY, "development");
assert.equal(process.env.MODE_LOCAL_ONLY, "1");
});
});
it("does not override variables that already exist in the shell environment", () => {
withTempWorkspace((workspaceRoot) => {
writeFileSync(join(workspaceRoot, ".env"), "DATABASE_URL=postgres://from-env\n");
writeFileSync(join(workspaceRoot, ".env.local"), "DATABASE_URL=postgres://from-local\n");
process.env.DATABASE_URL = "postgres://from-shell";
loadWorkspaceEnv({ workspaceRoot });
assert.equal(process.env.DATABASE_URL, "postgres://from-shell");
});
});
it("returns the candidate env paths even when files are missing", () => {
withTempWorkspace((workspaceRoot) => {
process.env.NODE_ENV = "test";
const envPaths = resolveWorkspaceEnvPaths({ workspaceRoot });
assert.deepEqual(
envPaths.map((entry) => entry.slice(workspaceRoot.length + 1)),
[".env", ".env.test", ".env.local", ".env.test.local"],
);
assert.deepEqual(loadWorkspaceEnv({ workspaceRoot }), []);
});
});
});
+33 -7
View File
@@ -1,4 +1,4 @@
import { readFileSync } from "node:fs";
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
@@ -7,10 +7,34 @@ function resolveWorkspaceRoot() {
return resolve(currentDir, "../../../");
}
export function loadWorkspaceEnv() {
const envPath = resolve(resolveWorkspaceRoot(), ".env");
export function resolveWorkspaceEnvPaths(options: { workspaceRoot?: string; nodeEnv?: string | undefined } = {}) {
const workspaceRoot = options.workspaceRoot ?? resolveWorkspaceRoot();
const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV;
const candidates = [".env"];
if (nodeEnv) {
candidates.push(`.env.${nodeEnv}`);
}
candidates.push(".env.local");
if (nodeEnv) {
candidates.push(`.env.${nodeEnv}.local`);
}
return [...new Set(candidates.map((candidate) => resolve(workspaceRoot, candidate)))];
}
export function loadWorkspaceEnv(options: { workspaceRoot?: string; nodeEnv?: string | undefined } = {}) {
const envPaths = resolveWorkspaceEnvPaths(options);
const originalKeys = new Set(Object.keys(process.env));
const loadedPaths: string[] = [];
for (const envPath of envPaths) {
if (!existsSync(envPath)) {
continue;
}
try {
const contents = readFileSync(envPath, "utf8");
for (const rawLine of contents.split(/\r?\n/u)) {
@@ -31,13 +55,15 @@ export function loadWorkspaceEnv() {
(rawValue.startsWith("'") && rawValue.endsWith("'"));
const value = quoted ? rawValue.slice(1, -1) : rawValue;
if (process.env[key] == null) {
if (!originalKeys.has(key)) {
process.env[key] = value;
}
}
} catch {
// Leave environment untouched when .env is not present in the workspace.
loadedPaths.push(envPath);
}
return loadedPaths;
}
export function resolveWorkspacePath(...segments: string[]) {