UNPKG

everything-dev

Version:

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

144 lines (142 loc) • 6.19 kB
const require_runtime = require('../_virtual/_rolldown/runtime.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let effect = require("effect"); let node_crypto = require("node:crypto"); let node_process = require("node:process"); node_process = require_runtime.__toESM(node_process, 1); let dotenv = require("dotenv"); let _clack_prompts = require("@clack/prompts"); _clack_prompts = require_runtime.__toESM(_clack_prompts, 1); //#region src/env/project-env.ts var EnvEnsureError = class extends effect.Data.TaggedError("EnvEnsureError") {}; var EnvLoadError = class extends effect.Data.TaggedError("EnvLoadError") {}; var EnvSyncError = class extends effect.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 effect.Context.Service()("everything-dev/ShellEnv") {}; const captureShellEnv = effect.Effect.sync(() => Object.fromEntries(Object.entries(node_process.default.env).filter((entry) => entry[1] != null))); const ShellEnvLive = (env) => effect.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.Effect.gen(function* () { if (Object.keys(generated).length === 0) return []; const envPath = (0, node_path.join)(configDir, ".env"); if (!(0, node_fs.existsSync)(envPath)) return []; const lines = yield* effect.Effect.try({ try: () => (0, node_fs.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.Effect.try({ try: () => (0, node_fs.writeFileSync)(envPath, updated.join("\n")), catch: (cause) => new EnvSyncError({ cause }) }); for (const { key, from, to } of drift) yield* effect.Effect.logInfo(`[env] ${key} updated: ${maskValue(key, from)} → ${maskValue(key, to)} (generated from resolved ports)`); } return drift; }); var ProjectEnv = class extends effect.Context.Service()("everything-dev/ProjectEnv") {}; const makeProjectEnv = () => { const loadedDirs = /* @__PURE__ */ new Set(); return { ensureFile: (configDir) => effect.Effect.try({ try: () => { const envPath = (0, node_path.join)(configDir, ".env"); const examplePath = (0, node_path.join)(configDir, ".env.example"); if ((0, node_fs.existsSync)(envPath) || !(0, node_fs.existsSync)(examplePath)) return false; const lines = (0, node_fs.readFileSync)(examplePath, "utf-8").split("\n"); const secret = (0, node_crypto.randomBytes)(32).toString("base64url"); const updated = lines.map((line) => { if (line.startsWith("BETTER_AUTH_SECRET=")) return `BETTER_AUTH_SECRET=${secret}`; return line; }).join("\n"); (0, node_fs.writeFileSync)(envPath, updated); _clack_prompts.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET"); return true; }, catch: (cause) => new EnvEnsureError({ cause }) }), load: (configDir, options) => effect.Effect.gen(function* () { if (!options?.force && loadedDirs.has(configDir)) return; const envPath = (0, node_path.join)(configDir, ".env"); if (!(0, node_fs.existsSync)(envPath)) return; yield* effect.Effect.try({ try: () => (0, dotenv.config)({ path: envPath, processEnv: node_process.default.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 = effect.Layer.sync(ProjectEnv, () => makeProjectEnv()); //#endregion exports.EnvEnsureError = EnvEnsureError; exports.EnvLoadError = EnvLoadError; exports.EnvSyncError = EnvSyncError; exports.ProjectEnv = ProjectEnv; exports.ProjectEnvLive = ProjectEnvLive; exports.ShellEnv = ShellEnv; exports.ShellEnvLive = ShellEnvLive; exports.captureShellEnv = captureShellEnv; exports.makeProjectEnv = makeProjectEnv; exports.syncEnvFile = syncEnvFile; //# sourceMappingURL=project-env.cjs.map