everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
403 lines (401 loc) • 16.9 kB
JavaScript
import { deleteProcessEnv, getProcessEnv, processEnvRecord, setProcessEnv } from "./env/process-env.mjs";
import { buildDescription, buildServiceDescriptorMap, buildServiceDescriptorMapFromPlan } from "./service-descriptor.mjs";
import { BosConfigSchema } from "./types.mjs";
import { buildRegistryConfigUrl } from "./fastkv.mjs";
import { buildRuntimeConfig, buildRuntimePluginsForConfig, findConfigPath, getHostDevelopmentPort, resolveConfigComposableEntries } from "./config.mjs";
import { run } from "./utils/run.mjs";
import { isRegistryStart, resolveStartConfigSource } from "./start-config-source.mjs";
import { isDebug } from "./dev-log-pipeline.mjs";
import { detectAutoStartContext, isDockerAvailable, runDockerComposeUp, shouldAutoStartDocker } from "./infra/docker.mjs";
import { openResolution, walkExtendsChain } from "./resolution/session.mjs";
import { isRailwayPublicProxyUrl } from "./db/driver.mjs";
import { ensureFreshDeps, findWorkspaceRoot } from "./workspace.mjs";
import { syncResolvedSharedDeps } from "./shared-deps.mjs";
import { generateCodeArtifacts } from "./code-artifacts.mjs";
import { PortAllocatorLive, detectLocalPackages } from "./app.mjs";
import { ProjectEnv, ProjectEnvLive, captureShellEnv } from "./env/project-env.mjs";
import { materializeViaLayer } from "./infra/materializer.mjs";
import { planInfra } from "./infra/planner.mjs";
import { preflightLocalInfra } from "./infra/preflight.mjs";
import { mergeGeneratedOverFileEnv } from "./orchestrator.mjs";
import { emitProgress, timedPhase } from "./progress.mjs";
import { Data, Effect, Layer } from "effect";
//#region src/dev-program.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") {};
var StartFetchFailed = class extends Data.TaggedError("StartFetchFailed") {};
var StartRemoteConfigMissing = class extends Data.TaggedError("StartRemoteConfigMissing") {};
var StartConfigMissing = class extends 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 walkExtendsChain(`bos://${account}/${gateway}`, {
env: "production",
registry,
visit: async () => {}
});
return resolveConfigComposableEntries(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) => timedPhase(timings, name, Effect.tryPromise({
try: fn,
catch: (cause) => new DevStepError({
phase: name,
cause
})
}));
const ensureEnvStep = (projectEnv, configDir) => projectEnv.ensureFile(configDir).pipe(Effect.mapError((cause) => new DevStepError({
phase: "ensure env",
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* ensureEnvStep(projectEnv, session.root);
yield* loadEnvStep(projectEnv, session.root);
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,
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* ensureEnvStep(projectEnv, session.root);
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 mergedEnv = yield* Effect.sync(() => mergeGeneratedOverFileEnv(plan.envGenerated, processEnvRecord(), shell));
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
};
});
const startBootstrap = (deps, input) => Effect.gen(function* () {
const shell = yield* captureShellEnv;
const projectEnv = yield* ProjectEnv;
const sessionRoot = deps.session?.root ?? process.cwd();
yield* loadEnvStep(projectEnv, sessionRoot);
yield* emitProgress({
phase: "config",
status: "running"
});
const bosEnv = input.env ?? (getProcessEnv("BOS_ENV") === "staging" ? "staging" : "production");
const explicitConfig = resolveStartConfigSource(input, {
BOS_ACCOUNT: getProcessEnv("BOS_ACCOUNT"),
BOS_GATEWAY: 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 = buildRegistryConfigUrl(account, domain, input.registry);
remoteConfig = yield* 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 ?? getHostDevelopmentPort(baseConfig.app.host.development);
const isStaging = bosEnv === "staging";
const runtimePlugins = yield* Effect.tryPromise({
try: () => buildRuntimePluginsForConfig(baseConfig, sessionRoot, "production"),
catch: (cause) => new DevStepError({
phase: "resolve runtime plugins",
cause
})
});
const runtimeConfig = yield* Effect.tryPromise({
try: () => 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* emitProgress({
phase: "generate artifacts",
status: "running"
});
yield* Effect.tryPromise({
try: () => generateCodeArtifacts(sessionRoot, baseConfig, {
env: "production",
runtimeConfig
}),
catch: (cause) => new DevStepError({
phase: "generate artifacts",
cause
})
});
yield* emitProgress({
phase: "generate artifacts",
status: "done"
});
const productionEnv = {};
const warnings = [];
const localhostOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
const corsOrigin = getProcessEnv("CORS_ORIGIN");
if (corsOrigin !== void 0 && localhostOrigin.test(corsOrigin) && isRegistryStart(explicitConfig)) {
warnings.push(`CORS_ORIGIN is a localhost origin (${corsOrigin}) in a registry production start — overriding with the configured domain`);
deleteProcessEnv("CORS_ORIGIN");
deleteProcessEnv("BASE_URL");
}
if (!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 = 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(processEnvRecord())) {
if (!value || !secret.endsWith("_DATABASE_URL")) continue;
if (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* 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 = buildServiceDescriptorMap(plan.runtimeConfig);
const summary = {
configSource: explicitConfig.configPath ? explicitConfig.configPath : remoteConfig ? `bos://${explicitConfig.registry?.account}/${explicitConfig.registry?.domain}` : findConfigPath() ?? "bos.config.json",
configSourceHttp: remoteConfig && explicitConfig.registry ? 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* 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 = Layer.mergeAll(ProjectEnvLive, PortAllocatorLive);
//#endregion
export { DevConfigMissing, DevPreflightFailed, DevProxyMissing, DevStepError, StartConfigMissing, StartFetchFailed, StartRemoteConfigMissing, bootstrapLayers, devBootstrap, resolveProxyUrl, startBootstrap };
//# sourceMappingURL=dev-program.mjs.map