UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

132 lines (130 loc) • 5.48 kB
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { Context, Data, Effect, Layer } from "effect"; import { randomBytes } from "node:crypto"; import process from "node:process"; import { config } from "dotenv"; import * as p from "@clack/prompts"; //#region src/env/project-env.ts var EnvEnsureError = class extends Data.TaggedError("EnvEnsureError") {}; var EnvLoadError = class extends Data.TaggedError("EnvLoadError") {}; var EnvSyncError = class extends Data.TaggedError("EnvSyncError") {}; /** * The process environment as exported by the parent shell. Values present * here were explicitly provided by the caller (shell, CI, the regression * harness) and therefore outrank planner-generated values; values that only * exist after `.env` loading do not. The bootstrap program captures this * snapshot as its first step — before any `.env` loading — and provides it * to the rest of the program as a service. */ var ShellEnv = class extends Context.Service()("everything-dev/ShellEnv") {}; const captureShellEnv = Effect.sync(() => Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] != null))); const ShellEnvLive = (env) => Layer.succeed(ShellEnv, env); /** * Align the bos-owned lines of `.env` with the generated infra env * (ports/secrets derived from the resolved dev topology). Keys the caller * explicitly exported (`shellEnv`) are skipped — a deliberate override must * not be silently reverted on disk. Lines the user added outside the * generated key set are untouched. Idempotent. */ const GENERATED_OWNED_KEYS = /* @__PURE__ */ new Set(["BASE_URL", "CORS_ORIGIN"]); const maskValue = (key, value) => { if (value === void 0) return "(absent)"; if (/_DATABASE_URL$|_REDIS_URL$|_SECRET$|_KEY$/.test(key)) return value.replace(/\/\/([^:/@]+):([^@]+)@/, "//***:***@"); return value; }; const syncEnvFile = (configDir, generated, shellEnv = {}) => Effect.gen(function* () { if (Object.keys(generated).length === 0) return []; const envPath = join(configDir, ".env"); if (!existsSync(envPath)) return []; const lines = yield* Effect.try({ try: () => readFileSync(envPath, "utf-8").split("\n"), catch: (cause) => new EnvSyncError({ cause }) }); const drift = []; const seen = /* @__PURE__ */ new Set(); const keyOf = (line) => { return /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line)?.[1] ?? null; }; const updated = lines.map((line) => { const key = keyOf(line); if (!key || !(key in generated)) return line; if (key in shellEnv && !GENERATED_OWNED_KEYS.has(key)) return line; seen.add(key); const value = line.slice(key.length + 1); if (value === generated[key]) return line; drift.push({ key, from: value, to: generated[key] }); return `${key}=${generated[key]}`; }); for (const [key, value] of Object.entries(generated)) { if (seen.has(key)) continue; if (key in shellEnv && !GENERATED_OWNED_KEYS.has(key)) continue; drift.push({ key, from: void 0, to: value }); updated.push(`${key}=${value}`); } if (drift.length > 0) { yield* Effect.try({ try: () => writeFileSync(envPath, updated.join("\n")), catch: (cause) => new EnvSyncError({ cause }) }); for (const { key, from, to } of drift) yield* Effect.logInfo(`[env] ${key} updated: ${maskValue(key, from)} → ${maskValue(key, to)} (generated from resolved ports)`); } return drift; }); var ProjectEnv = class extends Context.Service()("everything-dev/ProjectEnv") {}; const makeProjectEnv = () => { const loadedDirs = /* @__PURE__ */ new Set(); return { ensureFile: (configDir) => Effect.try({ try: () => { const envPath = join(configDir, ".env"); const examplePath = join(configDir, ".env.example"); if (existsSync(envPath) || !existsSync(examplePath)) return false; const lines = readFileSync(examplePath, "utf-8").split("\n"); const secret = randomBytes(32).toString("base64url"); const updated = lines.map((line) => { if (line.startsWith("BETTER_AUTH_SECRET=")) return `BETTER_AUTH_SECRET=${secret}`; return line; }).join("\n"); writeFileSync(envPath, updated); p.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET"); return true; }, catch: (cause) => new EnvEnsureError({ cause }) }), load: (configDir, options) => Effect.gen(function* () { if (!options?.force && loadedDirs.has(configDir)) return; const envPath = join(configDir, ".env"); if (!existsSync(envPath)) return; yield* Effect.try({ try: () => config({ path: envPath, processEnv: process.env, quiet: true }), catch: (cause) => new EnvLoadError({ cause }) }); loadedDirs.add(configDir); }), sync: (configDir, generated, shellEnv) => syncEnvFile(configDir, generated, shellEnv) }; }; /** * One `ProjectEnv` implementation, installed in two scopes: the CLI provides * `ProjectEnvLive` from `bootstrapLayers`, and the plugin runtime installs the * same layer with `buildScoped` so it finalizes with the plugin. `makeProjectEnv` * is that implementation; command and test callers that need a single shot use * the factory and do not register another service. */ const ProjectEnvLive = Layer.sync(ProjectEnv, () => makeProjectEnv()); //#endregion export { EnvEnsureError, EnvLoadError, EnvSyncError, ProjectEnv, ProjectEnvLive, ShellEnv, ShellEnvLive, captureShellEnv, makeProjectEnv, syncEnvFile }; //# sourceMappingURL=project-env.mjs.map