everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
177 lines (175 loc) • 7.04 kB
JavaScript
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