UNPKG

everything-dev

Version:

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

208 lines (206 loc) • 8.81 kB
import { getProcessEnv, operatorEnvRecord, setProcessEnv } from "../../env/process-env.mjs"; import { buildDescription, buildServiceDescriptorMapFromPlan } from "./service-descriptor.mjs"; import { run } from "../../utils/run.mjs"; import { openResolution } from "../resolution/session.mjs"; import { ProjectEnv, captureShellEnv } from "../../env/project-env.mjs"; import { generateCodeArtifacts } from "../codegen/artifacts.mjs"; import { ensureFreshDeps, findWorkspaceRoot } from "../workspace/workspace.mjs"; import { syncResolvedSharedDeps } from "../workspace/shared-deps.mjs"; import { materializeViaLayer } from "../../infra/materializer.mjs"; import { planInfra } from "../../infra/planner.mjs"; import { isDebug } from "./logs.mjs"; import { detectAutoStartContext, isDockerAvailable, runDockerComposeUp, shouldAutoStartDocker } from "../../infra/docker.mjs"; import { preflightLocalInfra } from "../../infra/preflight.mjs"; import { detectLocalPackages } from "../workspace/linked-packages.mjs"; import { mergeGeneratedOverFileEnv } from "./orchestrator.mjs"; import { timedPhase } from "./progress.mjs"; import { Data, Effect } from "effect"; import { randomBytes } from "node:crypto"; //#region src/services/dev/boot.ts var DevStepError = class extends Data.TaggedError("DevStepError") {}; var DevConfigMissing = class extends Data.TaggedError("DevConfigMissing") {}; var DevProxyMissing = class extends Data.TaggedError("DevProxyMissing") {}; var DevPreflightFailed = class extends Data.TaggedError("DevPreflightFailed") {}; function parseSourceMode(value, defaultValue) { if (value === "local" || value === "remote") return value; return defaultValue; } 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) => timedPhase(timings, name, Effect.tryPromise({ try: fn, catch: (cause) => new DevStepError({ phase: name, cause }) })); const loadEnvStep = (projectEnv, configDir) => projectEnv.load(configDir).pipe(Effect.mapError((cause) => new DevStepError({ phase: "load env", cause }))); const devBootstrap = (deps, input, timings) => Effect.gen(function* () { const shell = yield* captureShellEnv; const projectEnv = yield* ProjectEnv; if (!deps.session) return yield* new DevConfigMissing({}); let session = deps.session; yield* loadEnvStep(projectEnv, session.root); const operatorEnv = operatorEnvRecord(); const localPackages = 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.sync(() => { setProcessEnv("BOS_LOG_LEVEL", input.logLevel); }); if (ssr) yield* Effect.sync(() => { setProcessEnv("BOS_SSR", "1"); }); const sharedSync = yield* step(timings, "shared deps", () => syncResolvedSharedDeps({ configDir: session.root, hostMode: hostSource, bosConfig: session.config ?? void 0, extendsChain: [] })); let configMayHaveChanged = false; if (sharedSync.catalogChanged) { yield* step(timings, "install", () => run("pnpm", ["install"], { cwd: findWorkspaceRoot(session.root)?.dir ?? session.root })); configMayHaveChanged = true; } yield* step(timings, "build", async () => { if ((await ensureFreshDeps(session.root, localDirs)).rebuilt.some((member) => member.name === "everything-dev")) { if (import.meta.url.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 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.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.logWarning(message); const plan = yield* timedPhase(timings, "ports", planInfra({ configDir: session.root, bosConfig: developmentRuntime, operatorEnv, 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.tryPromise({ try: () => materializeViaLayer(session.root, plan.runtimeConfig), catch: (cause) => new DevStepError({ phase: "materialize infra", cause }) }); yield* loadEnvStep(projectEnv, session.root); yield* projectEnv.sync(session.root, plan.envGenerated, shell).pipe(Effect.catchTag("EnvSyncError", (error) => Effect.logWarning(`[env] failed to refresh .env from resolved ports: ${String(error.cause)}`))); const needsEphemeralSecret = !("BETTER_AUTH_SECRET" in operatorEnv) || operatorEnv.BETTER_AUTH_SECRET === ""; const mergedEnv = yield* Effect.sync(() => { if (needsEphemeralSecret) plan.envGenerated.BETTER_AUTH_SECRET = randomBytes(32).toString("base64url"); return mergeGeneratedOverFileEnv(plan.envGenerated, operatorEnvRecord(), shell); }); if (needsEphemeralSecret) yield* Effect.logInfo("[env] BETTER_AUTH_SECRET generated per-run (dev-only, not persisted — create a .env to keep it stable)"); yield* Effect.sync(() => { for (const [key, value] of Object.entries(mergedEnv)) if (key === "BASE_URL" || key === "CORS_ORIGIN") setProcessEnv(key, value); }); let preflightFailures = yield* preflightLocalInfra(plan.envGenerated, mergedEnv); if (preflightFailures.length > 0 && shouldAutoStartDocker(preflightFailures, { ...detectAutoStartContext(session.root), dockerAvailable: yield* Effect.promise(isDockerAvailable) })) { const compose = yield* step(timings, "docker compose up", () => runDockerComposeUp(session.root)); if (compose.ok) preflightFailures = yield* 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 = buildServiceDescriptorMapFromPlan(plan, { ssr, proxy }); const packages = [...plan.serviceDescriptors.keys()]; if (isDebug(getProcessEnv("DEBUG"))) yield* 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", () => 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: buildDescription(services) || plan.description, processes: packages }; }); //#endregion export { DevConfigMissing, DevPreflightFailed, DevProxyMissing, DevStepError, devBootstrap, loadEnvStep, resolveProxyUrl }; //# sourceMappingURL=boot.mjs.map