everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
525 lines (523 loc) • 20.9 kB
JavaScript
const require_service_descriptor = require('./service-descriptor.cjs');
const require_dev_log_pipeline = require('./dev-log-pipeline.cjs');
const require_mf = require('./mf.cjs');
const require_project_env = require('./env/project-env.cjs');
let effect = require("effect");
let node_child_process = require("node:child_process");
let node_stream = require("node:stream");
//#region src/orchestrator.ts
const warnOutsideEffect = (...args) => {
console.warn(...args);
};
const runFetch = (url, init) => fetch(url, init);
process.on("unhandledRejection", (reason) => {
console.error("[Orchestrator] Unhandled rejection:", reason);
});
process.on("uncaughtException", (err) => {
console.error("[Orchestrator] Uncaught exception:", err);
});
var OrchestratorError = class extends effect.Data.TaggedError("OrchestratorError") {
get message() {
return `${this.operation}: ${this.detail}`;
}
};
const omitUndefined = (value) => {
if (Array.isArray(value)) return value.map((item) => item === void 0 ? null : omitUndefined(item));
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).map(([key, item]) => [key, omitUndefined(item)]));
return value;
};
const encodeRuntimeConfig = (runtimeConfig) => effect.Schema.encodeUnknownEffect(effect.Schema.fromJsonString(effect.Schema.Json))(omitUndefined(runtimeConfig)).pipe(effect.Effect.mapError((cause) => new OrchestratorError({
operation: "serialize runtime config",
detail: cause.message,
cause
})));
const probeHttpOk = (url, timeoutMs = 400) => effect.Effect.tryPromise({
try: async () => {
try {
return (await runFetch(url, { signal: AbortSignal.timeout(timeoutMs) })).ok;
} catch {
return false;
}
},
catch: () => false
});
const LOCAL_PROBE_DEADLINE_MS = 9e4;
const LOCAL_PROBE_INTERVAL_MS = 200;
const cleanExitSignals = {
SIGTERM: true,
SIGINT: true
};
const REMOTE_PROBE_TIMEOUT_MS = 5e3;
const REMOTE_PROBE_DEADLINE_MS = 6e4;
const REMOTE_PROBE_BACKOFF_INITIAL_MS = 1e3;
const REMOTE_PROBE_BACKOFF_MAX_MS = 15e3;
const detectStatus = (line, descriptor) => {
const cleanLine = require_dev_log_pipeline.stripAnsi(line);
const errorPatterns = descriptor.errorPatterns ?? [];
const readyPatterns = descriptor.readyPatterns ?? [];
for (const pattern of readyPatterns) if (pattern.test(cleanLine)) return {
status: "ready",
isError: false
};
for (const pattern of errorPatterns) if (pattern.test(cleanLine)) return {
status: "error",
isError: true
};
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) {
const cause = arg.cause;
parts.push(`(cause: ${cause instanceof Error ? cause.message : typeof cause === "string" ? cause : JSON.stringify(cause) ?? "[Unserializable cause]"})`);
}
if (isError && arg.stack) parts.push(arg.stack);
return parts.join("\n");
}
if (arg === null) return "null";
if (typeof arg === "string") return arg;
if (typeof arg === "number" || typeof arg === "bigint" || typeof arg === "boolean") return `${arg}`;
if (typeof arg === "symbol") return arg.description ?? "";
if (typeof arg === "undefined") return "undefined";
return JSON.stringify(arg, null, 2) ?? "[Unserializable value]";
}).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;
};
};
var HostRemoteUrlMissing = class extends effect.Data.TaggedError("HostRemoteUrlMissing") {
get message() {
return "remoteUrl not provided on host descriptor";
}
};
var HostModuleInvalid = class extends effect.Data.TaggedError("HostModuleInvalid") {
get message() {
return "Host module does not export runServer function";
}
};
var LocalPathMissing = class extends effect.Data.TaggedError("LocalPathMissing") {
get message() {
return `No localPath for local service: ${this.key}`;
}
};
const spawnRemoteHost = (descriptor, callbacks) => effect.Effect.gen(function* () {
const runtimeConfig = yield* require_service_descriptor.DevRuntimeConfig;
const remoteUrl = descriptor.remoteUrl;
if (!remoteUrl) return yield* new HostRemoteUrlMissing();
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.Effect.tryPromise({
try: () => import("@module-federation/enhanced/runtime"),
catch: (cause) => new OrchestratorError({
operation: "load module federation runtime",
detail: cause instanceof Error ? cause.message : String(cause),
cause
})
});
const mfCore = yield* effect.Effect.tryPromise({
try: () => import("@module-federation/runtime-core"),
catch: (cause) => new OrchestratorError({
operation: "load module federation core",
detail: cause instanceof Error ? cause.message : String(cause),
cause
})
});
let mf = mfRuntime.getInstance();
if (!mf) {
mf = mfRuntime.createInstance({
name: "cli-host",
remotes: []
});
mfCore.setGlobalFederationInstance(mf);
}
require_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.Effect.tryPromise({
try: async () => {
try {
let res;
try {
res = await runFetch(manifestUrl, { signal: AbortSignal.timeout(1e4) });
} catch {
throw new Error("manifest fetch failed");
}
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 (e) {
warnOutsideEffect(`[Orchestrator] Failed to fetch or parse manifest from ${manifestUrl}, falling back to remoteEntryUrl: ${String(e)}`);
}
return remoteEntryUrl;
},
catch: () => remoteEntryUrl
});
mf.registerRemotes([{
name: "host",
entry: entryUrl
}]);
callbacks.onLog(descriptor.key, `Loading host from ${entryUrl}...`);
const hostModule = yield* effect.Effect.tryPromise({
try: () => mf.loadRemote("host/Server"),
catch: (cause) => new OrchestratorError({
operation: "load host module",
detail: cause instanceof Error ? cause.message : String(cause),
cause
})
});
if (!hostModule?.runServer) return yield* new HostModuleInvalid();
callbacks.onLog(descriptor.key, "Starting server...");
const hostPort = runtimeConfig.host?.port;
const hostEnv = hostPort ? { PORT: String(hostPort) } : void 0;
const serverHandle = hostModule.runServer({
config: runtimeConfig,
port: hostPort,
env: hostEnv
});
yield* effect.Effect.tryPromise({
try: () => serverHandle.ready,
catch: (cause) => new OrchestratorError({
operation: "start server",
detail: cause instanceof Error ? cause.message : String(cause),
cause
})
});
callbacks.onStatus(descriptor.key, "ready");
return {
name: descriptor.key,
pid: void 0,
kill: effect.Effect.gen(function* () {
callbacks.onLog(descriptor.key, "Shutting down remote host...");
restoreConsole();
yield* effect.Effect.tryPromise({
try: () => serverHandle.shutdown(),
catch: () => {}
}).pipe(effect.Effect.ignore);
}),
waitForReady: effect.Effect.void,
waitForExit: effect.Effect.never
};
});
/**
* Spawn env precedence, three tiers: values explicitly exported by the
* caller (shell / CI / regression harness — captured by the bootstrap
* program into the `ShellEnv` service before any `.env` loading) outrank the
* generated infra env, which outranks `.env`-file values inherited through
* `processEnv`. The service's resolved port is always authoritative.
*/
function composeSpawnEnv(processEnv, generatedEnv, port, shellEnv = {}) {
return {
...processEnv,
...mergeGeneratedOverFileEnv(generatedEnv, processEnv, shellEnv),
FORCE_COLOR: "1",
...port > 0 ? { PORT: String(port) } : {}
};
}
/**
* Overlay the generated infra env (ports drift, so stale `.env` values must
* lose) while keeping every explicitly exported key from `shellEnv` intact.
* Keys absent from both are untouched.
*
* Exception (ADR 0012 / plan 038): the origin keys BASE_URL and CORS_ORIGIN
* are always generated-owned — a wrapper that sourced a stale `.env` must not
* win over the resolved host port, in the child env or on disk.
*/
const GENERATED_OWNED_KEYS = /* @__PURE__ */ new Set(["BASE_URL", "CORS_ORIGIN"]);
function mergeGeneratedOverFileEnv(generatedEnv, processEnv, shellEnv = {}) {
const result = { ...processEnv };
for (const [key, value] of Object.entries(generatedEnv)) if (!GENERATED_OWNED_KEYS.has(key) && key in shellEnv) result[key] = shellEnv[key];
else result[key] = value;
return result;
}
const spawnDevProcess = (descriptor, callbacks) => effect.Effect.gen(function* () {
const runtimeConfig = yield* require_service_descriptor.DevRuntimeConfig;
if (!descriptor.localPath) return yield* new LocalPathMissing({ key: descriptor.key });
const fullCwd = descriptor.localPath;
const command = descriptor.command ?? "pnpm";
const args = descriptor.args ?? ["run", "dev"];
const port = descriptor.port ?? descriptor.defaultPort;
const name = descriptor.key;
const readyDeferred = yield* effect.Deferred.make();
const statusRef = yield* effect.Ref.make("starting");
callbacks.onStatus(name, "starting");
const generatedEnv = yield* require_service_descriptor.DevGeneratedEnv;
const shellTier = yield* require_project_env.ShellEnv;
const envVars = composeSpawnEnv(process.env, {
...generatedEnv,
...descriptor.env
}, port, shellTier);
envVars.BOS_RUNTIME_CONFIG = yield* encodeRuntimeConfig(runtimeConfig);
const cmd = (0, node_child_process.spawn)(command, args, {
cwd: fullCwd,
env: envVars,
stdio: [
"ignore",
"pipe",
"pipe"
],
detached: true
});
let lastExit = null;
const exitCode = effect.Effect.callback((resume) => {
const onExit = (code, signal) => {
lastExit = {
code,
signal
};
resume(effect.Effect.succeed(code ?? (signal ? 1 : 0)));
};
const onError = (err) => {
callbacks.onLog(name, `Spawn failed: ${err.message}`, true);
callbacks.onStatus(name, "error", `spawn failed: ${err.message}`);
resume(effect.Effect.fail(new OrchestratorError({
operation: "spawn process",
detail: err.message,
cause: err
})));
};
cmd.once("exit", onExit);
cmd.once("error", onError);
return effect.Effect.sync(() => {
cmd.off("exit", onExit);
cmd.off("error", onError);
});
});
const describeExit = (exitCodeValue) => {
if (lastExit?.code === null && lastExit.signal) return `signal: ${lastExit.signal}`;
return `exit code: ${exitCodeValue}`;
};
const isCleanSignalExit = () => lastExit?.code === null && lastExit.signal != null && lastExit.signal in cleanExitSignals;
const markReady = effect.Effect.gen(function* () {
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready" || currentStatus === "error") return;
yield* effect.Ref.set(statusRef, "ready");
callbacks.onStatus(name, "ready");
yield* effect.Deferred.succeed(readyDeferred, void 0).pipe(effect.Effect.ignore);
});
const markError = (message) => effect.Effect.gen(function* () {
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready" || currentStatus === "error") return;
yield* effect.Ref.set(statusRef, "error");
callbacks.onStatus(name, "error");
yield* effect.Deferred.fail(readyDeferred, new OrchestratorError({
operation: "wait for process readiness",
detail: message
})).pipe(effect.Effect.ignore);
});
yield* effect.Effect.forkScoped(effect.Effect.gen(function* () {
const readinessPath = descriptor.readinessPath;
const url = `http://127.0.0.1:${port}${readinessPath}`;
const readinessCheck = effect.Effect.gen(function* () {
const status = yield* effect.Ref.get(statusRef);
if (status === "ready" || status === "error") return true;
if (port <= 0) return false;
const ok = yield* probeHttpOk(url);
if (ok) yield* markReady;
return ok;
});
if (yield* effect.Effect.repeat(readinessCheck, {
schedule: effect.Schedule.spaced(`${port > 0 ? LOCAL_PROBE_INTERVAL_MS : 500} millis`),
until: (done) => done
}).pipe(effect.Effect.timeout(`${LOCAL_PROBE_DEADLINE_MS} millis`), effect.Effect.catchTag("TimeoutError", () => effect.Effect.succeed(false)))) return;
const status = yield* effect.Ref.get(statusRef);
if (status !== "ready" && status !== "error") {
callbacks.onLog(name, "Probe deadline exceeded after 90s", true);
yield* markError(`Probe deadline exceeded: ${name}`);
}
}));
const pid = cmd.pid ?? void 0;
yield* effect.Effect.forkScoped(effect.Effect.gen(function* () {
const exitCodeValue = yield* exitCode;
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready" || currentStatus === "error") {
if (currentStatus === "ready") {
const detail = describeExit(exitCodeValue);
callbacks.onLog(name, `Process exited after ready (${detail})`, !isCleanSignalExit());
if (!isCleanSignalExit()) callbacks.onStatus(name, "error", `exited after ready (${detail})`);
yield* markError(`Process exited after ready: ${name}`);
}
return;
}
callbacks.onLog(name, `Process exited before ready (${describeExit(exitCodeValue)})`, !isCleanSignalExit());
yield* markError(`Process exited before ready: ${name}`);
}));
const handleLine = (line, isStderr) => effect.Effect.gen(function* () {
if (!line.trim()) return;
const cleanLine = require_dev_log_pipeline.stripAnsi(line);
const looksLikeError = isStderr && /^(error|fail|fatal|exception|unhandled|reject)/i.test(cleanLine) && !cleanLine.startsWith("$");
callbacks.onLog(name, line, looksLikeError);
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready") return;
const detected = detectStatus(line, descriptor);
if (detected) {
if (detected.status === "ready") yield* markReady;
else if (currentStatus !== "error") yield* markError(`Process failed: ${name}`);
}
});
const stdoutStream = cmd.stdout ? node_stream.Readable.toWeb(cmd.stdout) : null;
const stderrStream = cmd.stderr ? node_stream.Readable.toWeb(cmd.stderr) : null;
if (stdoutStream) yield* effect.Effect.forkScoped(effect.Stream.runForEach((line) => handleLine(line, false))(effect.Stream.splitLines(effect.Stream.decodeText(effect.Stream.fromReadableStream({
evaluate: () => stdoutStream,
onError: (cause) => new OrchestratorError({
operation: "read process stdout",
detail: String(cause),
cause
})
})))));
if (stderrStream) yield* effect.Effect.forkScoped(effect.Stream.runForEach((line) => handleLine(line, true))(effect.Stream.splitLines(effect.Stream.decodeText(effect.Stream.fromReadableStream({
evaluate: () => stderrStream,
onError: (cause) => new OrchestratorError({
operation: "read process stderr",
detail: String(cause),
cause
})
})))));
return {
name,
pid,
kill: effect.Effect.gen(function* () {
const groupPid = pid;
const groupSignal = (signal) => effect.Effect.try(() => {
if (groupPid !== void 0) process.kill(-groupPid, signal);
}).pipe(effect.Effect.ignore);
yield* groupSignal("SIGTERM");
const exited = yield* exitCode.pipe(effect.Effect.timeout("3 seconds"), effect.Effect.option);
if (effect.Option.isNone(exited)) {
yield* groupSignal("SIGKILL");
yield* effect.Effect.sleep("250 millis");
}
}).pipe(effect.Effect.ignore),
waitForReady: effect.Deferred.await(readyDeferred),
waitForExit: exitCode
};
});
const spawnRemoteProbe = (pkg, descriptor, callbacks) => effect.Effect.gen(function* () {
callbacks.onStatus(pkg, "starting");
const readyDeferred = yield* effect.Deferred.make();
const statusRef = yield* effect.Ref.make("starting");
const markReady = effect.Effect.gen(function* () {
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready" || currentStatus === "error") return;
yield* effect.Ref.set(statusRef, "ready");
yield* effect.Deferred.succeed(readyDeferred, void 0).pipe(effect.Effect.ignore);
callbacks.onStatus(pkg, "ready", "loaded");
});
const markError = effect.Effect.gen(function* () {
const currentStatus = yield* effect.Ref.get(statusRef);
if (currentStatus === "ready" || currentStatus === "error") return;
yield* effect.Ref.set(statusRef, "error");
yield* effect.Deferred.fail(readyDeferred, new OrchestratorError({
operation: "probe remote process",
detail: `Remote ${pkg} unreachable`
})).pipe(effect.Effect.ignore);
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.Effect.forkScoped(effect.Effect.gen(function* () {
const readinessCheck = effect.Effect.gen(function* () {
const status = yield* effect.Ref.get(statusRef);
if (status === "ready" || status === "error") return true;
if (yield* probeHttpOk(probeUrl, REMOTE_PROBE_TIMEOUT_MS)) {
yield* markReady;
return true;
}
if (yield* probeHttpOk(entryUrl, REMOTE_PROBE_TIMEOUT_MS)) {
yield* markReady;
return true;
}
return false;
});
if (yield* effect.Effect.repeat(readinessCheck, {
schedule: effect.Schedule.min([effect.Schedule.exponential(`${REMOTE_PROBE_BACKOFF_INITIAL_MS} millis`, 1.5), effect.Schedule.spaced(`${REMOTE_PROBE_BACKOFF_MAX_MS} millis`)]),
until: (done) => done
}).pipe(effect.Effect.timeout(`${REMOTE_PROBE_DEADLINE_MS} millis`), effect.Effect.catchTag("TimeoutError", () => effect.Effect.succeed(false)))) return;
if ((yield* effect.Ref.get(statusRef)) !== "ready") yield* markError;
}));
return {
name: pkg,
pid: void 0,
kill: effect.Effect.gen(function* () {
yield* effect.Ref.set(statusRef, "error");
yield* effect.Deferred.fail(readyDeferred, new OrchestratorError({
operation: "stop remote process",
detail: "Killed"
})).pipe(effect.Effect.ignore);
}),
waitForReady: effect.Deferred.await(readyDeferred),
waitForExit: effect.Effect.never
};
});
const makeDevProcess = (pkg, callbacks, portOverride) => effect.Effect.gen(function* () {
const descriptor = (yield* require_service_descriptor.ServiceDescriptorMap).get(pkg);
if (!descriptor) {
callbacks.onStatus(pkg, "ready", "Remote");
return {
name: pkg,
pid: void 0,
kill: effect.Effect.void,
waitForReady: effect.Effect.void,
waitForExit: effect.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);
const resolvedDescriptor = portOverride ? {
...descriptor,
port: portOverride
} : descriptor;
return yield* spawnDevProcess(resolvedDescriptor, 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,
uiPort: descriptor?.uiPort
};
});
}
//#endregion
exports.HostModuleInvalid = HostModuleInvalid;
exports.HostRemoteUrlMissing = HostRemoteUrlMissing;
exports.LocalPathMissing = LocalPathMissing;
exports.OrchestratorError = OrchestratorError;
exports.composeSpawnEnv = composeSpawnEnv;
exports.detectStatus = detectStatus;
exports.encodeRuntimeConfig = encodeRuntimeConfig;
exports.getProcessStates = getProcessStates;
exports.makeDevProcess = makeDevProcess;
exports.mergeGeneratedOverFileEnv = mergeGeneratedOverFileEnv;
//# sourceMappingURL=orchestrator.cjs.map