UNPKG

everything-dev

Version:

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

321 lines (319 loc) 13.5 kB
const require_runtime = require('../_virtual/_rolldown/runtime.cjs'); let node_fs = require("node:fs"); let node_path = require("node:path"); let node_crypto = require("node:crypto"); let _clack_prompts = require("@clack/prompts"); _clack_prompts = require_runtime.__toESM(_clack_prompts, 1); let dotenv = require("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_POSTGRES_PORT = 5434; const BASE_REDIS_PORT = 6379; 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 loadPortState(configDir) { if (!configDir) return { postgresPorts: {}, redisPorts: {} }; const statePath = (0, node_path.join)(configDir, ".bos", "infra-state.json"); if (!(0, node_fs.existsSync)(statePath)) return { postgresPorts: {}, redisPorts: {} }; try { const raw = JSON.parse((0, node_fs.readFileSync)(statePath, "utf-8")); return { postgresPorts: raw.postgresPorts ?? {}, redisPorts: raw.redisPorts ?? {} }; } catch { return { postgresPorts: {}, redisPorts: {} }; } } function savePortState(configDir, state) { const statePath = (0, node_path.join)(configDir, ".bos", "infra-state.json"); (0, node_fs.mkdirSync)((0, node_path.dirname)(statePath), { recursive: true }); (0, node_fs.writeFileSync)(statePath, `${JSON.stringify(state, null, 2)}\n`); } function resolvePort(slug, portMap, basePort) { if (portMap[slug] !== void 0) return portMap[slug]; const assigned = Object.values(portMap); const next = assigned.length > 0 ? Math.max(...assigned) + 1 : basePort; portMap[slug] = next; return next; } function normalizeRedisSlug(secret) { return secret.replace(/_REDIS_URL$/, "").toLowerCase(); } 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, configDir) { const groups = getSecretGroups(runtimeConfig); const allSecrets = groups.flatMap((group) => group.secrets); const originMap = configDir ? buildOriginMap(configDir, runtimeConfig) : /* @__PURE__ */ new Map(); const portState = loadPortState(configDir); return { spec: { groups, databases: buildDatabaseConfigs(allSecrets, originMap, portState.postgresPorts), redis: buildRedisConfigs(allSecrets, originMap, portState.redisPorts) }, portState }; } function normalizeDatabaseSlug(secret) { return secret.replace(/_DATABASE_URL$/, "").toLowerCase(); } function buildOriginMap(configDir, runtimeConfig) { const configPath = (0, node_path.join)(configDir, "bos.config.json"); const originMap = /* @__PURE__ */ new Map(); const account = runtimeConfig.account; const resolveOrigin = (extendsRef) => { if (typeof extendsRef === "string") return extendsRef.match(/^bos:\/\/([^/]+)\//)?.[1] ?? null; return null; }; const rawConfig = (0, node_fs.existsSync)(configPath) ? JSON.parse((0, node_fs.readFileSync)(configPath, "utf-8")) : null; const rawPlugins = rawConfig?.plugins; for (const secret of runtimeConfig.api.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, account); const authExtends = ((rawConfig?.app)?.auth)?.extends; const authOrigin = resolveOrigin(authExtends) ?? account; for (const secret of runtimeConfig.auth?.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, authOrigin); for (const [pluginKey, pluginEntry] of Object.entries(runtimeConfig.plugins ?? {})) { const rawPlugin = rawPlugins?.[pluginKey]; let pluginOrigin; if (typeof rawPlugin === "string") pluginOrigin = resolveOrigin(rawPlugin) ?? account; else if (rawPlugin && typeof rawPlugin === "object") pluginOrigin = resolveOrigin(rawPlugin.extends) ?? account; else pluginOrigin = account; for (const secret of pluginEntry.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, pluginOrigin); } for (const secret of runtimeConfig.host.secrets ?? []) if (!originMap.has(secret)) originMap.set(secret, account); return originMap; } function buildDatabaseConfigs(secrets, originMap, portMap) { const orderedSecrets = [...uniqueSecrets(secrets.filter((secret) => secret.endsWith("_DATABASE_URL")))]; for (const secret of orderedSecrets) { const slug = normalizeDatabaseSlug(secret); if (secret === API_DATABASE_SECRET) portMap[slug] = 5432; else if (secret === AUTH_DATABASE_SECRET) portMap[slug] = 5433; else resolvePort(slug, portMap, BASE_POSTGRES_PORT); } return orderedSecrets.map((secret) => { const slug = normalizeDatabaseSlug(secret); const fromKey = originMap.get(secret) ?? ""; const port = portMap[slug]; const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_postgres_${slug}_data` : `postgres_${slug}_data`; const containerName = fromKey ? `${fromKey}-postgres-${slug}` : `postgres-${slug}`; return { secret, slug, fromKey, port, serviceName: `postgres-${slug.replace(/_/g, "-")}`, containerName, databaseName: `${slug}_db`, volumeName, url: `postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${port}/${slug}_db` }; }); } function buildRedisConfigs(secrets, originMap, portMap) { const redisSecrets = uniqueSecrets(secrets.filter((secret) => secret.endsWith("_REDIS_URL"))); for (const secret of redisSecrets) resolvePort(normalizeRedisSlug(secret), portMap, BASE_REDIS_PORT); return redisSecrets.map((secret) => { const slug = normalizeRedisSlug(secret); const fromKey = originMap.get(secret) ?? ""; const port = portMap[slug]; const volumeName = fromKey ? `${fromKey.replace(/\./g, "_")}_redis_${slug}_data` : `redis_${slug}_data`; const containerName = fromKey ? `${fromKey}-redis-${slug}` : `redis-${slug}`; return { secret, slug, fromKey, port, serviceName: `redis-${slug.replace(/_/g, "-")}`, containerName, volumeName, url: `redis://localhost:${port}` }; }); } function extractPortFromUrl(url) { return url.match(/:(\d{4,5})(?:\/|$)/)?.[1] ?? null; } function defaultSecretValue(secret, databases, redisConfigs, options) { if (secret === "BETTER_AUTH_SECRET") return options.forExample ? "" : (0, node_crypto.randomBytes)(32).toString("base64url"); if (secret === "CORS_ORIGIN") return "http://localhost:3000"; return databases.get(secret)?.url ?? redisConfigs.get(secret)?.url ?? ""; } function renderEnvFile(groups, databases, redisConfigs, options) { const databaseMap = new Map(databases.map((entry) => [entry.secret, entry])); const redisMap = new Map(redisConfigs.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, redisMap, options)}`); lines.push(""); } return `${lines.join("\n")}\n`; } function renderDockerCompose(databases, redisConfigs, projectName) { const lines = [`name: ${projectName}`, ""]; if (databases.length > 0) lines.push("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", ""); if (redisConfigs.length > 0) lines.push("x-redis-common: &redis-common", " image: redis:7-alpine", " command: redis-server --appendonly yes", " healthcheck:", " test: [\"CMD\", \"redis-cli\", \"ping\"]", " interval: 3s", " timeout: 3s", " retries: 5", ""); lines.push("services:"); for (const database of databases) { lines.push(` ${database.serviceName}:`); lines.push(" <<: *pg-common"); lines.push(` container_name: ${database.containerName}`); 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(""); } for (const redis of redisConfigs) { lines.push(` ${redis.serviceName}:`); lines.push(" <<: *redis-common"); lines.push(` container_name: ${redis.containerName}`); lines.push(" ports:"); lines.push(` - "${redis.port}:6379"`); lines.push(" volumes:"); lines.push(` - ${redis.volumeName}:/data`); lines.push(""); } lines.push("volumes:"); for (const database of databases) { lines.push(` ${database.volumeName}:`); lines.push(` name: ${database.volumeName}`); } for (const redis of redisConfigs) { lines.push(` ${redis.volumeName}:`); lines.push(` name: ${redis.volumeName}`); } return `${lines.join("\n")}\n`; } function syncTextFile(filePath, nextContent) { if ((0, node_fs.existsSync)(filePath) && (0, node_fs.readFileSync)(filePath, "utf-8") === nextContent) return false; (0, node_fs.writeFileSync)(filePath, nextContent); return true; } function writeGeneratedInfra(configDir, runtimeConfig) { const result = syncGeneratedInfra(configDir, runtimeConfig); if (result.staleEnvWarnings.length > 0) { _clack_prompts.log.warn(`.env has ${result.staleEnvWarnings.length} stale value(s) compared to .env.example:`); for (const warning of result.staleEnvWarnings) _clack_prompts.log.message(` ${warning}`); } return result.secrets; } function syncGeneratedInfra(configDir, runtimeConfig) { const { spec, portState } = buildGeneratedInfraSpec(runtimeConfig, configDir); const secrets = spec.groups.flatMap((group) => group.secrets); const newEnvContent = renderEnvFile(spec.groups, spec.databases, spec.redis, { forExample: true }); const newDockerContent = renderDockerCompose(spec.databases, spec.redis, runtimeConfig.account); const envExamplePath = (0, node_path.join)(configDir, ".env.example"); const dockerComposePath = (0, node_path.join)(configDir, "docker-compose.yml"); const staleWarnings = checkEnvStaleness(configDir, spec.databases, spec.redis); if (configDir) savePortState(configDir, portState); return { secrets, envExampleChanged: syncTextFile(envExamplePath, newEnvContent), dockerComposeChanged: syncTextFile(dockerComposePath, newDockerContent), staleEnvWarnings: staleWarnings }; } function checkEnvStaleness(configDir, databases, redisConfigs) { const envPath = (0, node_path.join)(configDir, ".env"); if (!(0, node_fs.existsSync)(envPath)) return []; const existingEnv = (0, node_fs.readFileSync)(envPath, "utf-8"); const envMap = /* @__PURE__ */ new Map(); for (const line of existingEnv.split("\n")) { const match = line.match(/^([A-Z_]+)=(.*)$/); if (match) envMap.set(match[1], match[2]); } const stale = []; for (const db of databases) { const existing = envMap.get(db.secret); if (existing && existing !== db.url) { const oldPort = extractPortFromUrl(existing) ?? "?"; stale.push(`${db.secret}: port ${oldPort}${db.port}`); } } for (const redis of redisConfigs) { const existing = envMap.get(redis.secret); if (existing && existing !== redis.url) { const oldPort = extractPortFromUrl(existing) ?? "?"; stale.push(`${redis.secret}: port ${oldPort}${redis.port}`); } } return stale; } function ensureEnvFile(configDir) { 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; const lines = (0, node_fs.readFileSync)(examplePath, "utf-8").split("\n"); const secret = (0, node_crypto.randomBytes)(32).toString("base64url"); (0, node_fs.writeFileSync)(envPath, lines.map((line) => { if (/^BETTER_AUTH_SECRET=/.test(line)) return `BETTER_AUTH_SECRET=${secret}`; return line; }).join("\n")); _clack_prompts.log.info("Created .env from generated .env.example with generated BETTER_AUTH_SECRET"); } function loadProjectEnv(configDir) { const envPath = (0, node_path.join)(configDir, ".env"); if (!(0, node_fs.existsSync)(envPath)) return; (0, dotenv.config)({ path: envPath, processEnv: process.env, quiet: true }); } //#endregion exports.ensureEnvFile = ensureEnvFile; exports.loadProjectEnv = loadProjectEnv; exports.syncGeneratedInfra = syncGeneratedInfra; exports.writeGeneratedInfra = writeGeneratedInfra; //# sourceMappingURL=infra.cjs.map