UNPKG

everything-dev

Version:

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

413 lines (411 loc) • 18.2 kB
const require_process_env = require('./env/process-env.cjs'); const require_service_descriptor = require('./service-descriptor.cjs'); const require_types = require('./types.cjs'); const require_fastkv = require('./fastkv.cjs'); const require_config = require('./config.cjs'); const require_run = require('./utils/run.cjs'); const require_start_config_source = require('./start-config-source.cjs'); const require_dev_log_pipeline = require('./dev-log-pipeline.cjs'); const require_docker = require('./infra/docker.cjs'); const require_resolution_session = require('./resolution/session.cjs'); const require_driver = require('./db/driver.cjs'); const require_workspace = require('./workspace.cjs'); const require_shared_deps = require('./shared-deps.cjs'); const require_code_artifacts = require('./code-artifacts.cjs'); const require_app = require('./app.cjs'); const require_project_env = require('./env/project-env.cjs'); const require_materializer = require('./infra/materializer.cjs'); const require_planner = require('./infra/planner.cjs'); const require_preflight = require('./infra/preflight.cjs'); const require_orchestrator = require('./orchestrator.cjs'); const require_progress = require('./progress.cjs'); let effect = require("effect"); //#region src/dev-program.ts var DevStepError = class extends effect.Data.TaggedError("DevStepError") {}; var DevConfigMissing = class extends effect.Data.TaggedError("DevConfigMissing") {}; var DevProxyMissing = class extends effect.Data.TaggedError("DevProxyMissing") {}; var DevPreflightFailed = class extends effect.Data.TaggedError("DevPreflightFailed") {}; var StartFetchFailed = class extends effect.Data.TaggedError("StartFetchFailed") {}; var StartRemoteConfigMissing = class extends effect.Data.TaggedError("StartRemoteConfigMissing") {}; var StartConfigMissing = class extends effect.Data.TaggedError("StartConfigMissing") {}; function parseSourceMode(value, defaultValue) { if (value === "local" || value === "remote") return value; return defaultValue; } async function resolvePublishedConfig(account, gateway, registry) { try { const { config: merged } = await require_resolution_session.walkExtendsChain(`bos://${account}/${gateway}`, { env: "production", registry, visit: async () => {} }); return require_config.resolveConfigComposableEntries(require_types.BosConfigSchema.parse(merged), process.cwd(), "production"); } catch (error) { if (error instanceof Error && error.message.startsWith("No config found")) return null; throw error; } } function isValidProxyUrl(url) { try { const parsed = new URL(url); return parsed.protocol === "http:" || parsed.protocol === "https:"; } catch { return false; } } function resolveProxyUrl(bosConfig) { if (!bosConfig) return null; const apiConfig = bosConfig.app.api; if (!apiConfig) return null; if (apiConfig.proxy && isValidProxyUrl(apiConfig.proxy)) return apiConfig.proxy; if (apiConfig.production && isValidProxyUrl(apiConfig.production)) return apiConfig.production; return null; } const step = (timings, name, fn) => require_progress.timedPhase(timings, name, effect.Effect.tryPromise({ try: fn, catch: (cause) => new DevStepError({ phase: name, cause }) })); const ensureEnvStep = (projectEnv, configDir) => projectEnv.ensureFile(configDir).pipe(effect.Effect.mapError((cause) => new DevStepError({ phase: "ensure env", cause }))); const loadEnvStep = (projectEnv, configDir) => projectEnv.load(configDir).pipe(effect.Effect.mapError((cause) => new DevStepError({ phase: "load env", cause }))); const devBootstrap = (deps, input, timings) => effect.Effect.gen(function* () { const shell = yield* require_project_env.captureShellEnv; const projectEnv = yield* require_project_env.ProjectEnv; if (!deps.session) return yield* new DevConfigMissing({}); let session = deps.session; yield* ensureEnvStep(projectEnv, session.root); yield* loadEnvStep(projectEnv, session.root); const localPackages = require_app.detectLocalPackages(session.config ?? void 0, session.runtime ?? void 0, session.root); const localPackageNames = new Set(localPackages.map((entry) => entry.name)); const localDirs = localPackages.map((entry) => entry.dir); const hostSource = localPackageNames.has("host") ? parseSourceMode(input.host, "local") : "remote"; const uiSource = localPackageNames.has("ui") ? parseSourceMode(input.ui, "local") : "remote"; const apiSource = localPackageNames.has("api") ? parseSourceMode(input.api, "local") : "remote"; const authSource = localPackageNames.has("auth") ? parseSourceMode(input.auth, "local") : "remote"; const ssr = input.ssr ?? false; const proxy = input.proxy ?? false; if (input.logLevel) yield* effect.Effect.sync(() => { require_process_env.setProcessEnv("BOS_LOG_LEVEL", input.logLevel); }); if (ssr) yield* effect.Effect.sync(() => { require_process_env.setProcessEnv("BOS_SSR", "1"); }); const sharedSync = yield* step(timings, "shared deps", () => require_shared_deps.syncResolvedSharedDeps({ configDir: session.root, hostMode: hostSource, bosConfig: session.config ?? void 0, extendsChain: [] })); let configMayHaveChanged = false; if (sharedSync.catalogChanged) { yield* step(timings, "install", () => require_run.run("pnpm", ["install"], { cwd: require_workspace.findWorkspaceRoot(session.root)?.dir ?? session.root })); configMayHaveChanged = true; } yield* step(timings, "build", async () => { if ((await require_workspace.ensureFreshDeps(session.root, localDirs)).rebuilt.some((member) => member.name === "everything-dev")) { if (require("url").pathToFileURL(__filename).href.includes("/dist/")) console.log("[dev] everything-dev was rebuilt — this session still runs the previous orchestrator build. Restart `bos dev` once to pick it up."); } }); let devExtendsChain; if (configMayHaveChanged || input.remotePlugins !== void 0) yield* step(timings, "resolve config", async () => { const opened = await require_resolution_session.openResolution({ cwd: session.root, remotePlugins: input.remotePlugins }); if (opened) { session = opened; devExtendsChain = [...opened.chain]; } }); if (!session.config) return yield* new DevConfigMissing({}); const bosConfig = session.config; if (proxy && !resolveProxyUrl(bosConfig)) return yield* new DevProxyMissing({}); const developmentRuntime = yield* effect.Effect.tryPromise({ try: async () => { return session.buildRuntime({ uiSource, apiSource, authSource, hostSource, env: "development", plugins: session.runtime?.plugins }); }, catch: (cause) => new DevStepError({ phase: "build runtime config", cause }) }); for (const message of session.warnings) yield* effect.Effect.logWarning(message); const plan = yield* require_progress.timedPhase(timings, "ports", require_planner.planInfra({ configDir: session.root, bosConfig: developmentRuntime, cli: { port: input.port, apiPort: input.apiPort, authPort: input.authPort, uiPort: input.uiPort, pluginPortStart: input.pluginPortStart, ssr, proxy, hostSource, uiSource, apiSource, authSource, interactive: input.interactive } })); yield* effect.Effect.tryPromise({ try: () => require_materializer.materializeViaLayer(session.root, plan.runtimeConfig), catch: (cause) => new DevStepError({ phase: "materialize infra", cause }) }); yield* ensureEnvStep(projectEnv, session.root); yield* loadEnvStep(projectEnv, session.root); yield* projectEnv.sync(session.root, plan.envGenerated, shell).pipe(effect.Effect.catchTag("EnvSyncError", (error) => effect.Effect.logWarning(`[env] failed to refresh .env from resolved ports: ${String(error.cause)}`))); const mergedEnv = yield* effect.Effect.sync(() => require_orchestrator.mergeGeneratedOverFileEnv(plan.envGenerated, require_process_env.processEnvRecord(), shell)); yield* effect.Effect.sync(() => { for (const [key, value] of Object.entries(mergedEnv)) if (key === "BASE_URL" || key === "CORS_ORIGIN") require_process_env.setProcessEnv(key, value); }); let preflightFailures = yield* require_preflight.preflightLocalInfra(plan.envGenerated, mergedEnv); if (preflightFailures.length > 0 && require_docker.shouldAutoStartDocker(preflightFailures, { ...require_docker.detectAutoStartContext(session.root), dockerAvailable: yield* effect.Effect.promise(require_docker.isDockerAvailable) })) { const compose = yield* step(timings, "docker compose up", () => require_docker.runDockerComposeUp(session.root)); if (compose.ok) preflightFailures = yield* require_preflight.preflightLocalInfra(plan.envGenerated, mergedEnv); else preflightFailures = [...preflightFailures, { secret: "docker-compose", host: "localhost", port: 0, error: `docker compose up -d --wait failed${compose.tail ? `:\n${compose.tail}` : ""}`, tcpReachable: false }]; } if (preflightFailures.length > 0) return yield* new DevPreflightFailed({ messages: preflightFailures.map((f) => f.error) }); const services = require_service_descriptor.buildServiceDescriptorMapFromPlan(plan, { ssr, proxy }); const packages = [...plan.serviceDescriptors.keys()]; if (require_dev_log_pipeline.isDebug(require_process_env.getProcessEnv("DEBUG"))) yield* effect.Effect.logError(`[DEBUG dev] services keys: ${packages.join(", ")}`); if (services.get("api")?.proxy) { const proxyUrl = resolveProxyUrl(bosConfig); if (proxyUrl) plan.orchestrator.env.API_PROXY = proxyUrl; } yield* step(timings, "generate artifacts", () => require_code_artifacts.generateCodeArtifacts(session.root, bosConfig, { env: "development", extendsChain: devExtendsChain, runtimeConfig: plan.runtimeConfig })); return { session: { orchestrator: plan.orchestrator, services, runtimeConfig: plan.runtimeConfig, envGenerated: plan.envGenerated, shellEnv: shell }, description: require_service_descriptor.buildDescription(services) || plan.description, processes: packages }; }); const startBootstrap = (deps, input) => effect.Effect.gen(function* () { const shell = yield* require_project_env.captureShellEnv; const projectEnv = yield* require_project_env.ProjectEnv; const sessionRoot = deps.session?.root ?? process.cwd(); yield* loadEnvStep(projectEnv, sessionRoot); yield* require_progress.emitProgress({ phase: "config", status: "running" }); const bosEnv = input.env ?? (require_process_env.getProcessEnv("BOS_ENV") === "staging" ? "staging" : "production"); const explicitConfig = require_start_config_source.resolveStartConfigSource(input, { BOS_ACCOUNT: require_process_env.getProcessEnv("BOS_ACCOUNT"), BOS_GATEWAY: require_process_env.getProcessEnv("BOS_GATEWAY") }); let config = null; let remoteConfig = null; if (explicitConfig.configPath) config = deps.session?.config ?? null; else if (explicitConfig.registry) { const { account, domain } = explicitConfig.registry; const expectedUrl = require_fastkv.buildRegistryConfigUrl(account, domain, input.registry); remoteConfig = yield* effect.Effect.tryPromise({ try: () => resolvePublishedConfig(account, domain, input.registry), catch: (error) => new StartFetchFailed({ message: `Failed to fetch config for bos://${account}/${domain}: ${error instanceof Error ? error.message : "Unknown error"}\nExpected URL: ${expectedUrl}` }) }); if (remoteConfig) config = remoteConfig; else return yield* new StartRemoteConfigMissing({ message: `No config found at bos://${account}/${domain}. Verify the account and gateway are correct and the config has been published.\nExpected URL: ${expectedUrl}` }); } else config = deps.session?.config ?? null; if (!config) return yield* new StartConfigMissing({}); if (!explicitConfig.configPath) { if (explicitConfig.registry?.account) config = { ...config, account: explicitConfig.registry.account }; if (explicitConfig.registry?.domain) config = { ...config, domain: explicitConfig.registry.domain }; } const baseConfig = config; const port = input.port ?? require_config.getHostDevelopmentPort(baseConfig.app.host.development); const isStaging = bosEnv === "staging"; const runtimePlugins = yield* effect.Effect.tryPromise({ try: () => require_config.buildRuntimePluginsForConfig(baseConfig, sessionRoot, "production"), catch: (cause) => new DevStepError({ phase: "resolve runtime plugins", cause }) }); const runtimeConfig = yield* effect.Effect.tryPromise({ try: () => require_config.buildRuntimeConfig(baseConfig, sessionRoot, "production", { uiSource: "remote", apiSource: "remote", authSource: "remote", hostSource: "remote", plugins: runtimePlugins }), catch: (cause) => new DevStepError({ phase: "build runtime config", cause }) }); if (isStaging && baseConfig.staging?.domain) runtimeConfig.domain = baseConfig.staging.domain; if (isStaging) runtimeConfig.env = "staging"; yield* require_progress.emitProgress({ phase: "generate artifacts", status: "running" }); yield* effect.Effect.tryPromise({ try: () => require_code_artifacts.generateCodeArtifacts(sessionRoot, baseConfig, { env: "production", runtimeConfig }), catch: (cause) => new DevStepError({ phase: "generate artifacts", cause }) }); yield* require_progress.emitProgress({ phase: "generate artifacts", status: "done" }); const productionEnv = {}; const warnings = []; const localhostOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i; const corsOrigin = require_process_env.getProcessEnv("CORS_ORIGIN"); if (corsOrigin !== void 0 && localhostOrigin.test(corsOrigin) && require_start_config_source.isRegistryStart(explicitConfig)) { warnings.push(`CORS_ORIGIN is a localhost origin (${corsOrigin}) in a registry production start — overriding with the configured domain`); require_process_env.deleteProcessEnv("CORS_ORIGIN"); require_process_env.deleteProcessEnv("BASE_URL"); } if (!require_process_env.getProcessEnv("CORS_ORIGIN") && baseConfig.domain) { const defaultOrigin = `https://${isStaging ? baseConfig.staging?.domain ?? baseConfig.domain : baseConfig.domain}`; productionEnv.CORS_ORIGIN = defaultOrigin; productionEnv.BASE_URL = defaultOrigin; warnings.push(`CORS_ORIGIN defaulting to ${defaultOrigin}`); } const requiredSecrets = /* @__PURE__ */ new Set(); const missingSecrets = []; if (runtimeConfig.host.secrets) for (const s of runtimeConfig.host.secrets) requiredSecrets.add(s); if (runtimeConfig.auth?.secrets) for (const s of runtimeConfig.auth.secrets) requiredSecrets.add(s); if (runtimeConfig.api?.secrets) for (const s of runtimeConfig.api.secrets) requiredSecrets.add(s); for (const plugin of Object.values(runtimeConfig.plugins ?? {})) if (plugin.secrets) for (const s of plugin.secrets) { if (s.endsWith("_DATABASE_URL")) continue; requiredSecrets.add(s); } for (const secret of requiredSecrets) { const value = require_process_env.getProcessEnv(secret); if (!value || value.length === 0) missingSecrets.push(secret); } if (missingSecrets.length > 0) warnings.push(`Missing ${missingSecrets.length} secret(s): ${missingSecrets.join(", ")}`); for (const [secret, value] of Object.entries(require_process_env.processEnvRecord())) { if (!value || !secret.endsWith("_DATABASE_URL")) continue; if (require_driver.isRailwayPublicProxyUrl(value)) { const host = new URL(value).hostname; warnings.push(`${secret} points at Railway's public proxy (${host}) — its TLS certificate is self-signed, so connections use unverified TLS. Prefer the private *.railway.internal URL when the database runs in the same Railway project.`); } } const stagingEnvVars = isStaging ? { BOS_GATEWAY: baseConfig.staging?.domain ?? baseConfig.domain ?? "" } : {}; const plan = yield* require_planner.planInfra({ configDir: sessionRoot, bosConfig: runtimeConfig, cli: { port: input.port, ssr: false, proxy: false, hostSource: "remote", uiSource: "remote", apiSource: "remote", authSource: "remote", interactive: input.interactive } }); const services = require_service_descriptor.buildServiceDescriptorMap(plan.runtimeConfig); const summary = { configSource: explicitConfig.configPath ? explicitConfig.configPath : remoteConfig ? `bos://${explicitConfig.registry?.account}/${explicitConfig.registry?.domain}` : require_config.findConfigPath() ?? "bos.config.json", configSourceHttp: remoteConfig && explicitConfig.registry ? require_fastkv.buildRegistryConfigUrl(explicitConfig.registry.account, explicitConfig.registry.domain, input.registry) : void 0, account: baseConfig.account, domain: baseConfig.domain ?? void 0, modules: { host: plan.runtimeConfig.host.remoteUrl ?? plan.runtimeConfig.host.url ?? "local", ui: plan.runtimeConfig.ui.url ?? "local", api: plan.runtimeConfig.api.url ?? "local", auth: plan.runtimeConfig.auth?.url ?? void 0 }, warnings }; const orchestrator = { packages: ["host"], env: { NODE_ENV: "production", ...productionEnv, ...stagingEnvVars, ...plan.launch.env }, description: `${isStaging ? "Staging" : "Production"} Mode (${baseConfig.account})`, port: plan.resolvedPorts.host ?? port, interactive: input.interactive, noLogs: true }; yield* require_progress.emitProgress({ phase: "config", status: "done" }); return { session: { orchestrator, services, runtimeConfig: plan.runtimeConfig, envGenerated: plan.envGenerated, shellEnv: shell }, summary, url: plan.launch.hostUrl ?? `http://localhost:${plan.resolvedPorts.host ?? port}` }; }); const bootstrapLayers = effect.Layer.mergeAll(require_project_env.ProjectEnvLive, require_app.PortAllocatorLive); //#endregion exports.DevConfigMissing = DevConfigMissing; exports.DevPreflightFailed = DevPreflightFailed; exports.DevProxyMissing = DevProxyMissing; exports.DevStepError = DevStepError; exports.StartConfigMissing = StartConfigMissing; exports.StartFetchFailed = StartFetchFailed; exports.StartRemoteConfigMissing = StartRemoteConfigMissing; exports.bootstrapLayers = bootstrapLayers; exports.devBootstrap = devBootstrap; exports.resolveProxyUrl = resolveProxyUrl; exports.startBootstrap = startBootstrap; //# sourceMappingURL=dev-program.cjs.map