everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
144 lines (142 loc) • 6.19 kB
JavaScript
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