import { existsSync, readFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; type LoadMonorepoRootEnvOptions = { cwd?: string; env?: NodeJS.ProcessEnv; }; const parseEnvFile = (content: string): Record => { const values: Record = {}; for (const rawLine of content.split(/\r?\n/u)) { const line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) { continue; } const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u.exec(rawLine); if (!match) { continue; } const key = match[1]!; let value = match[2] ?? ""; value = value.trim(); if ( (value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'")) ) { const quote = value[0]!; value = value.slice(1, -1); if (quote === "\"") { value = value .replace(/\\n/gu, "\n") .replace(/\\r/gu, "\r") .replace(/\\t/gu, "\t") .replace(/\\"/gu, "\"") .replace(/\\\\/gu, "\\"); } } else { value = value.replace(/\s+#.*$/u, ""); } values[key] = value; } return values; }; const isWorkspaceRoot = (directory: string): boolean => { const packageJsonPath = join(directory, "package.json"); if (!existsSync(packageJsonPath)) { return false; } try { const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { workspaces?: unknown; }; return Array.isArray(parsed.workspaces); } catch { return false; } }; const findMonorepoRoot = (startDir: string): string | null => { let current = resolve(startDir); while (true) { if (isWorkspaceRoot(current)) { return current; } const parent = dirname(current); if (parent === current) { return null; } current = parent; } }; export const loadMonorepoRootEnv = ( options: LoadMonorepoRootEnvOptions = {}, ): { rootDir: string | null; loadedFiles: string[] } => { const cwd = options.cwd ?? process.cwd(); const env = options.env ?? process.env; const rootDir = findMonorepoRoot(cwd); if (rootDir === null) { return { rootDir: null, loadedFiles: [], }; } const initialKeys = new Set(Object.keys(env)); const loadedFiles: string[] = []; for (const fileName of [".env", ".env.local"]) { const filePath = join(rootDir, fileName); if (!existsSync(filePath)) { continue; } const parsed = parseEnvFile(readFileSync(filePath, "utf8")); for (const [key, value] of Object.entries(parsed)) { if (initialKeys.has(key)) { continue; } env[key] = value; } loadedFiles.push(filePath); } return { rootDir, loadedFiles, }; };