UNPKG

everything-dev

Version:

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

316 lines (314 loc) 12.3 kB
import { patchManifestFetchForSsrPublicPath } from "./mf.mjs"; import { DevRuntimeConfig, ServiceDescriptorMap } from "./service-descriptor.mjs"; import { Deferred, Effect, Option, Ref, Stream } from "effect"; import { Command } from "@effect/platform"; //#region src/orchestrator.ts process.on("unhandledRejection", (reason) => { console.error("[Orchestrator] Unhandled rejection:", reason); }); process.on("uncaughtException", (err) => { console.error("[Orchestrator] Uncaught exception:", err); }); const stripAnsi = (input) => { const ESC = String.fromCharCode(27); const BEL = String.fromCharCode(7); return input.replace(new RegExp(`${ESC}\\][^${BEL}]*${BEL}`, "g"), "").replace(new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, "g"), ""); }; const probeHttpOk = (url, timeoutMs = 400) => Effect.tryPromise({ try: async () => { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { return (await fetch(url, { signal: controller.signal })).ok; } catch { return false; } finally { clearTimeout(timer); } }, catch: () => false }); const detectStatus = (line, descriptor) => { const cleanLine = stripAnsi(line); const errorPatterns = descriptor.errorPatterns ?? []; const readyPatterns = descriptor.readyPatterns ?? []; for (const pattern of errorPatterns) if (pattern.test(cleanLine)) return { status: "error", isError: true }; for (const pattern of readyPatterns) if (pattern.test(cleanLine)) return { status: "ready", isError: false }; return null; }; const patchConsole = (name, callbacks) => { const originalLog = console.log; const originalError = console.error; const originalWarn = console.warn; const originalInfo = console.info; const formatArgs = (args, isError = false) => { return args.map((arg) => { if (arg instanceof Error) { const parts = [`${arg.name}: ${arg.message}`]; if (arg.cause instanceof Error) parts.push(`(cause: ${arg.cause.name}: ${arg.cause.message})`); else if (arg.cause) parts.push(`(cause: ${String(arg.cause)})`); if (isError && arg.stack) parts.push(arg.stack); return parts.join("\n"); } return typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg); }).join(" "); }; console.log = (...args) => { callbacks.onLog(name, formatArgs(args), false); }; console.error = (...args) => { callbacks.onLog(name, formatArgs(args, true), true); }; console.warn = (...args) => { callbacks.onLog(name, formatArgs(args), false); }; console.info = (...args) => { callbacks.onLog(name, formatArgs(args), false); }; return () => { console.log = originalLog; console.error = originalError; console.warn = originalWarn; console.info = originalInfo; }; }; const spawnRemoteHost = (descriptor, callbacks) => Effect.gen(function* () { const runtimeConfig = yield* DevRuntimeConfig; const remoteUrl = descriptor.remoteUrl; if (!remoteUrl) return yield* Effect.fail(/* @__PURE__ */ new Error("remoteUrl not provided on host descriptor")); callbacks.onStatus(descriptor.key, "starting"); callbacks.onLog(descriptor.key, `Remote: ${remoteUrl}`); const restoreConsole = patchConsole(descriptor.key, callbacks); callbacks.onLog(descriptor.key, "Loading Module Federation runtime..."); const mfRuntime = yield* Effect.tryPromise({ try: () => import("@module-federation/enhanced/runtime"), catch: (e) => /* @__PURE__ */ new Error(`Failed to load MF runtime: ${e}`) }); const mfCore = yield* Effect.tryPromise({ try: () => import("@module-federation/runtime-core"), catch: (e) => /* @__PURE__ */ new Error(`Failed to load MF core: ${e}`) }); let mf = mfRuntime.getInstance(); if (!mf) { mf = mfRuntime.createInstance({ name: "cli-host", remotes: [] }); mfCore.setGlobalFederationInstance(mf); } patchManifestFetchForSsrPublicPath(mf); const baseUrl = remoteUrl.replace(/\/remoteEntry\.js$/, "").replace(/\/mf-manifest\.json$/, "").replace(/\/$/, ""); const remoteEntryUrl = `${baseUrl}/remoteEntry.js`; const manifestUrl = `${baseUrl}/mf-manifest.json`; const entryUrl = yield* Effect.tryPromise({ try: async () => { try { const res = await fetch(manifestUrl); if (!res.ok) return remoteEntryUrl; const json = await res.json(); if (json && typeof json === "object" && "metaData" in json && "exposes" in json && "shared" in json) return manifestUrl; } catch {} return remoteEntryUrl; }, catch: () => remoteEntryUrl }); mf.registerRemotes([{ name: "host", entry: entryUrl }]); callbacks.onLog(descriptor.key, `Loading host from ${entryUrl}...`); const hostModule = yield* Effect.tryPromise({ try: () => mf.loadRemote("host/Server"), catch: (e) => /* @__PURE__ */ new Error(`Failed to load host module: ${e}`) }); if (!hostModule?.runServer) return yield* Effect.fail(/* @__PURE__ */ new Error("Host module does not export runServer function")); callbacks.onLog(descriptor.key, "Starting server..."); const serverHandle = hostModule.runServer({ config: runtimeConfig }); yield* Effect.tryPromise({ try: () => serverHandle.ready, catch: (e) => /* @__PURE__ */ new Error(`Server failed to start: ${e}`) }); callbacks.onStatus(descriptor.key, "ready"); return { name: descriptor.key, pid: process.pid, kill: Effect.gen(function* () { callbacks.onLog(descriptor.key, "Shutting down remote host..."); restoreConsole(); yield* Effect.tryPromise({ try: () => serverHandle.shutdown(), catch: () => {} }).pipe(Effect.ignore); }), waitForReady: Effect.succeed(void 0), waitForExit: Effect.never }; }); const spawnDevProcess = (descriptor, callbacks) => Effect.gen(function* () { const runtimeConfig = yield* DevRuntimeConfig; if (!descriptor.localPath) return yield* Effect.fail(/* @__PURE__ */ new Error(`No localPath for local service: ${descriptor.key}`)); const fullCwd = descriptor.localPath; const command = descriptor.command ?? "bun"; const args = descriptor.args ?? ["run", "dev"]; const port = descriptor.port ?? descriptor.defaultPort; const name = descriptor.key; const readyDeferred = yield* Deferred.make(); const statusRef = yield* Ref.make("starting"); callbacks.onStatus(name, "starting"); const envVars = { ...process.env, FORCE_COLOR: "1", ...port > 0 ? { PORT: String(port) } : {} }; if (name === "host") envVars.BOS_RUNTIME_CONFIG = JSON.stringify(runtimeConfig); const cmd = Command.make(command, ...args).pipe(Command.workingDirectory(fullCwd), Command.env(envVars)); const proc = yield* Command.start(cmd); const markReady = Effect.gen(function* () { const currentStatus = yield* Ref.get(statusRef); if (currentStatus === "ready" || currentStatus === "error") return; yield* Ref.set(statusRef, "ready"); callbacks.onStatus(name, "ready"); yield* Deferred.succeed(readyDeferred, void 0).pipe(Effect.ignore); }); if (port > 0) { const url = `http://127.0.0.1:${port}${descriptor.readinessPath}`; yield* Effect.forkScoped(Effect.gen(function* () { const deadline = Date.now() + 9e4; while (Date.now() < deadline) { const status = yield* Ref.get(statusRef); if (status === "ready" || status === "error") return; if (yield* probeHttpOk(url)) { yield* markReady; return; } yield* Effect.sleep("200 millis"); } })); } const pid = Number(proc.pid); yield* Effect.forkScoped(Effect.gen(function* () { const exitCode = yield* proc.exitCode; const currentStatus = yield* Ref.get(statusRef); if (currentStatus === "ready" || currentStatus === "error") return; callbacks.onLog(name, `Process exited before ready (exit code: ${exitCode})`, true); yield* Ref.set(statusRef, "error"); callbacks.onStatus(name, "error"); yield* Deferred.fail(readyDeferred, /* @__PURE__ */ new Error(`Process exited before ready: ${name}`)).pipe(Effect.ignore); })); const handleLine = (line, isStderr) => Effect.gen(function* () { if (!line.trim()) return; const cleanLine = stripAnsi(line); const looksLikeError = isStderr && /^(error|fail|fatal|exception|unhandled|reject)/i.test(cleanLine) && !/^\$/.test(cleanLine); callbacks.onLog(name, line, looksLikeError); const currentStatus = yield* Ref.get(statusRef); if (currentStatus === "ready" || currentStatus === "error") return; const detected = detectStatus(line, descriptor); if (detected) { yield* Ref.set(statusRef, detected.status); callbacks.onStatus(name, detected.status); if (detected.status === "ready" || detected.status === "error") if (detected.status === "ready") yield* Deferred.succeed(readyDeferred, void 0).pipe(Effect.ignore); else yield* Deferred.fail(readyDeferred, /* @__PURE__ */ new Error(`Process failed: ${name}`)).pipe(Effect.ignore); } }); yield* Effect.forkScoped(Stream.runForEach((line) => handleLine(line, false))(Stream.splitLines(Stream.decodeText(proc.stdout, "utf-8")))); yield* Effect.forkScoped(Stream.runForEach((line) => handleLine(line, true))(Stream.splitLines(Stream.decodeText(proc.stderr, "utf-8")))); return { name, pid, kill: Effect.gen(function* () { const result = yield* proc.kill("SIGTERM").pipe(Effect.timeout("3 seconds"), Effect.option); if (Option.isNone(result)) { const pid = Number(proc.pid); yield* Effect.try(() => process.kill(-pid, "SIGKILL")).pipe(Effect.ignore); yield* Effect.sleep("250 millis"); } }).pipe(Effect.ignore), waitForReady: Deferred.await(readyDeferred), waitForExit: proc.exitCode }; }); const spawnRemoteProbe = (pkg, descriptor, callbacks) => Effect.gen(function* () { callbacks.onStatus(pkg, "starting"); const readyDeferred = yield* Deferred.make(); const statusRef = yield* Ref.make("starting"); const markReady = Effect.gen(function* () { yield* Ref.set(statusRef, "ready"); yield* Deferred.succeed(readyDeferred, void 0); callbacks.onStatus(pkg, "ready", "loaded"); }); const markError = Effect.gen(function* () { yield* Ref.set(statusRef, "error"); yield* Deferred.fail(readyDeferred, /* @__PURE__ */ new Error(`Remote ${pkg} unreachable`)); callbacks.onStatus(pkg, "error", "unreachable"); }); const baseUrl = descriptor.url.replace(/\/$/, ""); const manifestUrl = `${baseUrl}/mf-manifest.json`; const entryUrl = `${baseUrl}${descriptor.readinessPath}`; const probeUrl = descriptor.readinessPath === "/health" ? `${baseUrl}/health` : manifestUrl; yield* Effect.forkScoped(Effect.gen(function* () { const deadline = Date.now() + 6e4; while (Date.now() < deadline) { const status = yield* Ref.get(statusRef); if (status === "ready" || status === "error") return; if (yield* probeHttpOk(probeUrl, 400)) { yield* markReady; return; } if (yield* probeHttpOk(entryUrl, 400)) { yield* markReady; return; } yield* Effect.sleep("500 millis"); } if ((yield* Ref.get(statusRef)) !== "ready") yield* markError; })); return { name: pkg, pid: void 0, kill: Effect.gen(function* () { yield* Ref.set(statusRef, "error"); yield* Deferred.fail(readyDeferred, /* @__PURE__ */ new Error("Killed")).pipe(Effect.ignore); }), waitForReady: Deferred.await(readyDeferred), waitForExit: Effect.never }; }); const makeDevProcess = (pkg, callbacks, portOverride) => Effect.gen(function* () { const descriptor = (yield* ServiceDescriptorMap).get(pkg); if (!descriptor) { callbacks.onStatus(pkg, "ready", "Remote"); return { name: pkg, pid: void 0, kill: Effect.void, waitForReady: Effect.void, waitForExit: Effect.never }; } if (pkg === "host" && descriptor.source === "remote") return yield* spawnRemoteHost(descriptor, callbacks); if (descriptor.source === "remote" || !descriptor.localPath) return yield* spawnRemoteProbe(pkg, descriptor, callbacks); return yield* spawnDevProcess(portOverride ? { ...descriptor, port: portOverride } : descriptor, callbacks); }); function getProcessStates(packages, services, portOverride) { return packages.map((pkg) => { const descriptor = services.get(pkg); return { name: pkg, status: "pending", port: portOverride && pkg === "host" ? portOverride : descriptor?.port ?? descriptor?.defaultPort ?? 0, source: descriptor?.source }; }); } //#endregion export { getProcessStates, makeDevProcess }; //# sourceMappingURL=orchestrator.mjs.map