UNPKG

everything-dev

Version:

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

170 lines (168 loc) 6.2 kB
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { randomBytes } from "node:crypto"; import * as p from "@clack/prompts"; import { config } from "dotenv"; //#region src/cli/infra.ts const POSTGRES_USER = "everythingdev"; const POSTGRES_PASSWORD = "everythingdev"; const API_DATABASE_SECRET = "API_DATABASE_URL"; const AUTH_DATABASE_SECRET = "AUTH_DATABASE_URL"; const HOST_SECRET = "CORS_ORIGIN"; const BASE_DATABASE_PORT = 5434; function uniqueSecrets(values) { const secrets = []; const seen = /* @__PURE__ */ new Set(); for (const value of values) { if (!value || seen.has(value)) continue; seen.add(value); secrets.push(value); } return secrets; } function getSecretGroups(runtimeConfig) { const groups = []; const seen = /* @__PURE__ */ new Set(); const addGroup = (section, secrets) => { const filtered = secrets.filter((s) => { if (seen.has(s)) return false; seen.add(s); return true; }); if (filtered.length > 0) groups.push({ section, secrets: filtered }); }; addGroup("app.host", uniqueSecrets([...runtimeConfig.host.secrets ?? [], HOST_SECRET])); addGroup("app.api", uniqueSecrets(runtimeConfig.api.secrets ?? [])); if (runtimeConfig.auth) addGroup("app.auth", uniqueSecrets(runtimeConfig.auth.secrets ?? [])); if (runtimeConfig.plugins) { for (const [pluginKey, plugin] of Object.entries(runtimeConfig.plugins)) if (plugin.secrets && plugin.secrets.length > 0) addGroup(`plugins.${pluginKey}`, plugin.secrets); } return groups; } function buildGeneratedInfraSpec(runtimeConfig) { const groups = getSecretGroups(runtimeConfig); return { groups, databases: buildDatabaseConfigs(groups.flatMap((group) => group.secrets)) }; } function normalizeDatabaseSlug(secret) { return secret.replace(/_DATABASE_URL$/, "").toLowerCase(); } function buildDatabaseConfigs(secrets) { return [ API_DATABASE_SECRET, AUTH_DATABASE_SECRET, ...uniqueSecrets(secrets.filter((secret) => secret.endsWith("_DATABASE_URL"))).filter((secret) => secret !== API_DATABASE_SECRET && secret !== AUTH_DATABASE_SECRET).sort((a, b) => a.localeCompare(b)) ].map((secret, index) => { const slug = normalizeDatabaseSlug(secret); const port = secret === API_DATABASE_SECRET ? 5432 : secret === AUTH_DATABASE_SECRET ? 5433 : BASE_DATABASE_PORT + index - 2; return { secret, slug, port, serviceName: `postgres-${slug.replace(/_/g, "-")}`, databaseName: `${slug}_db`, volumeName: `postgres_${slug}_data`, url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db` }; }); } function defaultSecretValue(secret, databases, options) { if (secret === "BETTER_AUTH_SECRET") return options.forExample ? "" : randomBytes(32).toString("base64url"); if (secret === "CORS_ORIGIN") return "http://localhost:3000"; return databases.get(secret)?.url ?? ""; } function renderEnvFile(groups, databases, options) { const databaseMap = new Map(databases.map((entry) => [entry.secret, entry])); const lines = [ "# Generated from configured bos secrets", "# Update values as needed for your local environment", "" ]; for (const group of groups) { lines.push(`# ${group.section}`); for (const secret of group.secrets) lines.push(`${secret}=${defaultSecretValue(secret, databaseMap, options)}`); lines.push(""); } return `${lines.join("\n")}\n`; } function renderDockerCompose(databases) { const lines = [ "x-pg-common: &pg-common", " image: postgres:17-alpine", " environment: &pg-env", ` POSTGRES_USER: ${POSTGRES_USER}`, ` POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}`, " healthcheck:", " test: [\"CMD-SHELL\", \"pg_isready -U everythingdev\"]", " interval: 3s", " timeout: 3s", " retries: 5", "", "services:" ]; for (const database of databases) { lines.push(` ${database.serviceName}:`); lines.push(" <<: *pg-common"); lines.push(" environment:"); lines.push(" <<: *pg-env"); lines.push(` POSTGRES_DB: ${database.databaseName}`); lines.push(" ports:"); lines.push(` - "${database.port}:5432"`); lines.push(" volumes:"); lines.push(` - ${database.volumeName}:/var/lib/postgresql/data`); lines.push(""); } lines.push("volumes:"); for (const database of databases) lines.push(` ${database.volumeName}:`); return `${lines.join("\n")}\n`; } function syncTextFile(filePath, nextContent) { if (existsSync(filePath) && readFileSync(filePath, "utf-8") === nextContent) return false; writeFileSync(filePath, nextContent); return true; } function writeGeneratedInfra(configDir, runtimeConfig) { return syncGeneratedInfra(configDir, runtimeConfig).secrets; } function syncGeneratedInfra(configDir, runtimeConfig) { const spec = buildGeneratedInfraSpec(runtimeConfig); const secrets = spec.groups.flatMap((group) => group.secrets); const newEnvContent = renderEnvFile(spec.groups, spec.databases, { forExample: true }); const newDockerContent = renderDockerCompose(spec.databases); const envExamplePath = join(configDir, ".env.example"); const dockerComposePath = join(configDir, "docker-compose.yml"); return { secrets, envExampleChanged: syncTextFile(envExamplePath, newEnvContent), dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent) }; } function ensureEnvFile(configDir) { const envPath = join(configDir, ".env"); const examplePath = join(configDir, ".env.example"); if (existsSync(envPath) || !existsSync(examplePath)) return; const lines = readFileSync(examplePath, "utf-8").split("\n"); const secret = randomBytes(32).toString("base64url"); writeFileSync(envPath, lines.map((line) => { if (/^BETTER_AUTH_SECRET=/.test(line)) return `BETTER_AUTH_SECRET=${secret}`; return line; }).join("\n")); p.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET"); } function loadProjectEnv(configDir) { const envPath = join(configDir, ".env"); if (!existsSync(envPath)) return; config({ path: envPath, processEnv: process.env, quiet: true }); } //#endregion export { ensureEnvFile, loadProjectEnv, syncGeneratedInfra, writeGeneratedInfra }; //# sourceMappingURL=infra.mjs.map