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