UNPKG

everything-dev

Version:

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

177 lines (175 loc) 7.04 kB
import { getProjectRoot } from "./config.mjs"; import { DevRuntimeConfigLive, ServiceDescriptorMap, ServiceDescriptorMapLive } from "./service-descriptor.mjs"; import { renderDevView } from "./components/dev-view.mjs"; import { renderStreamingView } from "./components/streaming-view.mjs"; import { createDevLogger } from "./dev-logs.mjs"; import { getProcessStates, makeDevProcess } from "./orchestrator.mjs"; import { Deferred, Effect, Exit } from "effect"; import * as NodeContext from "@effect/platform-node/NodeContext"; //#region src/dev-session.ts const LOG_NOISE_PATTERNS = [ /\[ Federation Runtime \] Version .* from (host|ui) of shared singleton module/, /Executing an Effect versioned \d+\.\d+\.\d+ with a Runtime of version/, /you may want to dedupe the effect dependencies/ ]; const SSR_LOG_ALLOWLIST = [ /\bready\s+built in\b/i, /\bcompiled\b.*successfully/i, /\berror\b/i, /\bfailed\b/i, /\bexception\b/i ]; const shouldDisplayLog = (source, line, isError) => { if (process.env.DEBUG === "true" || process.env.DEBUG === "1") return true; if (source === "ui-ssr") { if (isError) return true; return SSR_LOG_ALLOWLIST.some((pattern) => pattern.test(line)); } return !LOG_NOISE_PATTERNS.some((pattern) => pattern.test(line)); }; const isInteractiveSupported = () => { return process.stdin.isTTY === true && process.stdout.isTTY === true; }; const STARTUP_ORDER = [ "ui-ssr", "ui", "auth", "api", "plugin", "host-build", "host" ]; const sortByOrder = (packages) => { return [...packages].sort((a, b) => { const aIdx = a.startsWith("plugin:") ? STARTUP_ORDER.indexOf("plugin") : STARTUP_ORDER.indexOf(a); const bIdx = b.startsWith("plugin:") ? STARTUP_ORDER.indexOf("plugin") : STARTUP_ORDER.indexOf(b); if (aIdx === -1 && bIdx === -1) return 0; if (aIdx === -1) return 1; if (bIdx === -1) return -1; return aIdx - bIdx; }); }; function formatLogLine(entry) { const ts = new Date(entry.timestamp).toISOString(); const prefix = entry.isError ? "ERR" : "OUT"; return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`; } const runDevSession = (orchestrator, onShutdownReady) => Effect.gen(function* () { const configDir = getProjectRoot(); const services = yield* ServiceDescriptorMap; const orderedPackages = sortByOrder(orchestrator.packages); const initialProcesses = getProcessStates(orderedPackages, services, orchestrator.port); const logger = yield* Effect.promise(() => createDevLogger(configDir, orchestrator.description)); const shutdown = yield* Deferred.make(); onShutdownReady?.(() => { Effect.runPromise(Deferred.succeed(shutdown, void 0)); }); const allLogs = []; let view = null; let shouldExportLogs = false; const requestShutdownAndExport = () => { shouldExportLogs = true; Effect.runPromise(Deferred.succeed(shutdown, void 0)); }; view = orchestrator.interactive ?? isInteractiveSupported() ? renderDevView(initialProcesses, orchestrator.description, orchestrator.env, () => void Effect.runPromise(Deferred.succeed(shutdown, void 0)), requestShutdownAndExport) : renderStreamingView(initialProcesses, orchestrator.description, orchestrator.env, () => void Effect.runPromise(Deferred.succeed(shutdown, void 0))); const callbacks = { onStatus: (name, status, message) => { view?.updateProcess(name, status, message); }, onLog: (name, line, isError) => { const entry = { id: `${Date.now()}-${allLogs.length + 1}`, source: name, line, timestamp: Date.now(), isError }; allLogs.push(entry); if (shouldDisplayLog(name, line, isError)) view?.addLog(name, line, isError); if (!orchestrator.noLogs) logger.write(entry); } }; const startProcess = (pkg) => { return makeDevProcess(pkg, callbacks, pkg === "host" ? orchestrator.port : void 0).pipe(Effect.tapError((err) => Effect.sync(() => { callbacks.onLog(pkg, `Failed to start: ${err}`, true); callbacks.onStatus(pkg, "error"); })), Effect.catchAll(() => Effect.succeed({ name: pkg, pid: void 0, kill: Effect.void, waitForReady: Effect.void, waitForExit: Effect.never }))); }; const startGroup = (packages) => Effect.forEach(packages, startProcess, { concurrency: "unbounded" }); const awaitReady = (_pkg, handle) => handle.waitForReady.pipe(Effect.catchAll(() => Effect.void)); const nonHostPackages = orderedPackages.filter((pkg) => pkg !== "host"); const hostPackages = orderedPackages.filter((pkg) => pkg === "host"); const nonHostHandles = yield* startGroup(nonHostPackages); yield* Effect.forEach(nonHostHandles.map((handle, index) => ({ handle, pkg: nonHostPackages[index] ?? handle.name })), ({ handle, pkg }) => awaitReady(pkg, handle), { concurrency: "unbounded" }); const hostHandles = yield* startGroup(hostPackages); yield* Effect.forEach(hostHandles.map((handle, index) => ({ handle, pkg: hostPackages[index] ?? handle.name })), ({ handle, pkg }) => awaitReady(pkg, handle), { concurrency: "unbounded" }); const allHandles = [...nonHostHandles, ...hostHandles]; yield* Effect.addFinalizer(() => Effect.gen(function* () { yield* Effect.forEach(allHandles, (h) => h.kill.pipe(Effect.ignore), { concurrency: "unbounded" }); yield* Effect.sleep("200 millis"); view?.unmount(); if (shouldExportLogs) { console.log("\n"); console.log("═".repeat(70)); console.log(` SESSION LOGS: ${orchestrator.description}`); console.log(` Started: ${new Date(allLogs[0]?.timestamp || Date.now()).toISOString()}`); console.log(` Total entries: ${allLogs.length}`); console.log("═".repeat(70)); console.log(""); for (const entry of allLogs) console.log(formatLogLine(entry)); console.log(""); console.log("═".repeat(70)); console.log(` Full logs saved to: ${logger.logFile}`); console.log("═".repeat(70)); console.log(""); } })); yield* Deferred.await(shutdown); }); const runApp = (orchestrator, services, runtimeConfig) => { let requestShutdown = null; let signalCount = 0; let forceExitTimer = null; const forceExit = () => { console.log("\n[Dev] Force exit"); process.exit(0); }; const program = Effect.scoped(runDevSession(orchestrator, (shutdown) => { requestShutdown = shutdown; })).pipe(Effect.provide(ServiceDescriptorMapLive(services)), Effect.provide(DevRuntimeConfigLive(runtimeConfig)), Effect.provide(NodeContext.layer), Effect.catchAllDefect((defect) => Effect.sync(() => { console.error("[Dev] Unhandled defect in orchestrator:", defect); }))); const handleSignal = () => { signalCount++; if (signalCount > 1) { forceExit(); return; } console.log("\n[Dev] Shutting down..."); forceExitTimer = setTimeout(forceExit, 5e3); requestShutdown?.(); }; process.on("SIGINT", handleSignal); process.on("SIGTERM", handleSignal); Effect.runPromiseExit(program).then((exit) => { if (forceExitTimer) clearTimeout(forceExitTimer); process.exit(Exit.isSuccess(exit) ? 0 : 0); }); }; const devApp = runApp; const startApp = runApp; //#endregion export { devApp, startApp }; //# sourceMappingURL=dev-session.mjs.map