UNPKG

everything-dev

Version:

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

291 lines (289 loc) • 12.7 kB
const require_process_env = require('./env/process-env.cjs'); const require_service_descriptor = require('./service-descriptor.cjs'); const require_dev_log_pipeline = require('./dev-log-pipeline.cjs'); const require_process_registry = require('./process-registry.cjs'); const require_app = require('./app.cjs'); const require_project_env = require('./env/project-env.cjs'); const require_port_ownership = require('./infra/port-ownership.cjs'); const require_orchestrator = require('./orchestrator.cjs'); const require_dev_logs = require('./dev-logs.cjs'); const require_process_kill = require('./process-kill.cjs'); const require_dev_render = require('./components/dev-render.cjs'); let effect = require("effect"); //#region src/dev-session.ts const adoptOrphanedChildren = (configDir) => { try { const entries = require_process_registry.readRegistry().filter((entry) => entry.pid > 1 && entry.configDir === configDir && !require_process_registry.isPidAlive(entry.pid)); if (entries.length === 0) return; const reaped = []; for (const entry of entries) for (const childPid of entry.childPids ?? []) { if (childPid === process.pid) continue; if (require_process_registry.isPidAlive(childPid)) { require_process_kill.reapGroup(childPid); reaped.push(childPid); } } if (reaped.length > 0) console.error(`[Dev] Reaped ${reaped.length} orphaned child process(es) from dead session(s): ${reaped.join(", ")}`); const deadPids = new Set(entries.map((entry) => entry.pid)); require_process_registry.writeRegistry(require_process_registry.readRegistry().filter((entry) => !deadPids.has(entry.pid))); } catch {} }; const isInteractiveSupported = () => { return process.stdin.isTTY === true && process.stdout.isTTY === true; }; const STARTUP_ORDER = [ "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; }); }; const runDevSession = (configDir, orchestrator, onShutdownReady) => effect.Effect.gen(function* () { adoptOrphanedChildren(configDir); const services = yield* require_service_descriptor.ServiceDescriptorMap; const runtimeConfig = yield* require_service_descriptor.DevRuntimeConfig; const orderedPackages = sortByOrder(orchestrator.packages); const initialProcesses = require_orchestrator.getProcessStates(orderedPackages, services, orchestrator.port); if (require_dev_log_pipeline.isDebug(require_process_env.getProcessEnv("DEBUG"))) { yield* effect.Effect.logError(`[DEBUG session] orchestrator.packages: ${orchestrator.packages.join(", ")}`); yield* effect.Effect.logError(`[DEBUG session] orderedPackages: ${orderedPackages.join(", ")}`); yield* effect.Effect.logError(`[DEBUG session] services keys: ${[...services.keys()].join(", ")}`); yield* effect.Effect.logError(`[DEBUG session] initialProcesses: ${initialProcesses.map((p) => `${p.name}=${p.source}`).join(", ")}`); } const logger = yield* effect.Effect.promise(() => require_dev_logs.createDevLogger(configDir, orchestrator.description)); const shutdown = yield* effect.Deferred.make(); const effectContext = yield* effect.Effect.context(); const controls = { requestShutdown: () => { effect.Effect.runPromiseWith(effectContext)(effect.Deferred.succeed(shutdown, void 0)); }, emergencyKill: () => {}, requestShutdownEscalating: () => {}, forceExit: () => {}, restoreView: () => {}, rearmForceExitTimer: () => {} }; onShutdownReady?.(controls); const isWorkspaceChild = require_process_env.getProcessEnv("BOS_WORKSPACE_CHILD") === "1"; let ownedPorts = []; if (!isWorkspaceChild) { const regPorts = {}; const addPort = (key, value) => { if (value != null) regPorts[key] = value; }; addPort("host", runtimeConfig.host.port); addPort("api", runtimeConfig.api.port); addPort("ui", runtimeConfig.ui.port); addPort("auth", runtimeConfig.auth?.port); if (runtimeConfig.plugins) for (const [id, plugin] of Object.entries(runtimeConfig.plugins)) { if (!require_service_descriptor.isAuthMirrorPluginEntry(runtimeConfig.auth, id, plugin)) addPort(`plugin:${id}`, plugin.port); addPort(`plugin-ui:${id}`, plugin.ui?.port); } require_process_registry.registerStandalone({ pid: process.pid, configDir, ports: regPorts, startedAt: yield* effect.Clock.currentTimeMillis, description: orchestrator.description }); ownedPorts = Object.values(regPorts); } let view = null; let shouldExportLogs = false; const logLevel = require_dev_log_pipeline.resolveLogLevel(); const allLogs = []; const pipeline = require_dev_log_pipeline.createLogPipeline({ level: logLevel, sinks: { display: (event) => { view?.addLog(event.source, event.line, event.level === "error"); }, file: (event) => { if (orchestrator.noLogs) return; logger.write(event); }, export: (event) => { allLogs.push(event); } } }); const useInteractive = isInteractiveSupported() && orchestrator.interactive !== false; view = require_dev_render.createDevRenderer(initialProcesses, orchestrator.description, orchestrator.env, () => controls.requestShutdownEscalating(), () => { shouldExportLogs = true; controls.requestShutdownEscalating(); }, { interactive: useInteractive, onForceExit: () => controls.forceExit() }); controls.restoreView = () => { if (useInteractive) view?.unmount(); }; const callbacks = { onStatus: (name, status, message) => { view?.updateProcess(name, status, message); }, onLog: (name, line, isError) => { pipeline.ingest({ source: name, line, isError }); } }; const spawned = []; controls.emergencyKill = () => { for (const handle of spawned) { if (handle.pid === void 0) continue; require_process_kill.reapGroup(handle.pid); } }; yield* effect.Effect.addFinalizer(() => effect.Effect.gen(function* () { controls.rearmForceExitTimer(); yield* effect.Effect.forEach(spawned, (h) => h.kill.pipe(effect.Effect.ignore), { concurrency: "unbounded" }); yield* effect.Effect.sleep("200 millis"); for (const port of ownedPorts) if (!(yield* require_app.probePortBindable(port))) { const owner = yield* require_port_ownership.ownerOfPort(port); yield* effect.Effect.logError(`[Dev] Port ${port} still bound after teardown${owner ? ` — pid ${owner.pid} (${owner.command})` : " — owner unknown (lsof unavailable)"}`); } if (!isWorkspaceChild) yield* effect.Effect.sync(() => { try { require_process_registry.unregisterPid(process.pid); } catch {} }); pipeline.flush(); view?.unmount(); if (shouldExportLogs) { const now = yield* effect.Clock.currentTimeMillis; const startedMs = allLogs[0]?.timestamp || now; yield* effect.Effect.log(""); yield* effect.Effect.log("═".repeat(70)); yield* effect.Effect.log(` SESSION LOGS: ${orchestrator.description}`); yield* effect.Effect.log(` Started: ${effect.DateTime.formatIso(effect.DateTime.makeUnsafe(startedMs))}`); yield* effect.Effect.log(` Filtered entries: ${allLogs.length} (level: ${logLevel})`); yield* effect.Effect.log("═".repeat(70)); yield* effect.Effect.log(""); for (const event of allLogs) yield* effect.Effect.log(require_dev_logs.formatLogLine(event)); yield* effect.Effect.log(""); yield* effect.Effect.log("═".repeat(70)); yield* effect.Effect.log(` Full logs saved to: ${logger.logFile}`); yield* effect.Effect.log("═".repeat(70)); yield* effect.Effect.log(""); } })); const startProcess = (pkg) => { const portOverride = pkg === "host" ? orchestrator.port : void 0; return require_orchestrator.makeDevProcess(pkg, callbacks, portOverride).pipe(effect.Effect.tap((handle) => effect.Effect.sync(() => spawned.push(handle))), effect.Effect.flatMap((handle) => effect.Effect.acquireRelease(effect.Effect.succeed(handle), () => handle.kill.pipe(effect.Effect.ignore))), effect.Effect.tapError((err) => effect.Effect.sync(() => { callbacks.onLog(pkg, `Failed to start: ${String(err)}`, true); callbacks.onStatus(pkg, "error"); })), effect.Effect.orElseSucceed(() => ({ name: pkg, pid: void 0, kill: effect.Effect.void, waitForReady: effect.Effect.void, waitForExit: effect.Effect.never }))); }; const startGroup = (packages) => effect.Effect.forEach(packages, startProcess, { concurrency: "unbounded" }); const awaitReady = (pkg, handle) => handle.waitForReady.pipe(effect.Effect.timeout("120 seconds"), effect.Effect.catch((err) => effect.Effect.sync(() => { callbacks.onLog(pkg, `Timed out or failed: ${err instanceof Error ? err.message : String(err)}`, true); }))); const nonHostPackages = orderedPackages.filter((pkg) => pkg !== "host"); const hostPackages = orderedPackages.filter((pkg) => pkg === "host"); const startupPhase = effect.Effect.gen(function* () { const nonHostHandles = yield* startGroup(nonHostPackages); yield* effect.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.Effect.forEach(hostHandles.map((handle, index) => ({ handle, pkg: hostPackages[index] ?? handle.name })), ({ handle, pkg }) => awaitReady(pkg, handle), { concurrency: "unbounded" }); return { nonHostHandles, hostHandles }; }); const startup = yield* effect.Effect.raceFirst(startupPhase, effect.Deferred.await(shutdown).pipe(effect.Effect.as(null))); if (startup === null) return; const childPids = [...startup.nonHostHandles, ...startup.hostHandles].map((handle) => Number(handle.pid)).filter((pid) => Number.isFinite(pid) && pid > 1 && pid !== process.pid); if (!isWorkspaceChild && childPids.length > 0) yield* effect.Effect.sync(() => { try { require_process_registry.updateChildPids(process.pid, childPids); } catch {} }); yield* effect.Deferred.await(shutdown); }); const runApp = (configDir, orchestrator, services, runtimeConfig, envGenerated, shellEnv) => { let controls = null; let signalCount = 0; let forceExitTimer = null; const forceExit = () => { controls?.restoreView(); console.log("\n[Dev] Force exit"); controls?.emergencyKill(); process.exit(0); }; const initialPpid = process.ppid; const orphanWatch = setInterval(() => { if (process.ppid !== initialPpid) { console.error("\n[Dev] Parent process died — force exiting (orphaned service tree)"); forceExit(); } }, 200); orphanWatch.unref?.(); const requestShutdownEscalating = () => { signalCount++; if (signalCount > 1) { forceExit(); return; } controls?.restoreView(); console.log("\n[Dev] Shutting down..."); forceExitTimer = setTimeout(forceExit, 5e3); forceExitTimer.unref?.(); controls?.requestShutdown(); }; const handleSignal = requestShutdownEscalating; const program = effect.Effect.scoped(runDevSession(configDir, orchestrator, (sessionControls) => { controls = sessionControls; sessionControls.requestShutdownEscalating = requestShutdownEscalating; sessionControls.rearmForceExitTimer = () => { if (forceExitTimer) clearTimeout(forceExitTimer); forceExitTimer = setTimeout(forceExit, 5e3); forceExitTimer.unref?.(); }; sessionControls.forceExit = forceExit; })).pipe(effect.Effect.provide(effect.Layer.mergeAll(require_service_descriptor.ServiceDescriptorMapLive(services), require_service_descriptor.DevRuntimeConfigLive(runtimeConfig), require_service_descriptor.DevGeneratedEnvLive(envGenerated), require_project_env.ShellEnvLive(shellEnv))), effect.Effect.catchDefect((defect) => effect.Effect.logError("[Dev] Unhandled defect in orchestrator:", defect).pipe(effect.Effect.andThen(effect.Effect.die(defect))))); process.on("SIGINT", handleSignal); process.on("SIGTERM", handleSignal); effect.Effect.runPromiseExit(program).then((exit) => { clearInterval(orphanWatch); if (forceExitTimer) clearTimeout(forceExitTimer); process.exit(effect.Exit.isSuccess(exit) ? 0 : 1); }).catch((error) => { clearInterval(orphanWatch); if (forceExitTimer) clearTimeout(forceExitTimer); console.error("[Dev] Failed to observe application exit:", error); process.exit(1); }); }; const devApp = runApp; const startApp = runApp; //#endregion exports.devApp = devApp; exports.runDevSession = runDevSession; exports.startApp = startApp; //# sourceMappingURL=dev-session.cjs.map