import { createInstance as createMFInstance } from "@module-federation/runtime"; import type { ModuleFederationRuntimePlugin, UserOptions, } from "@module-federation/runtime/types"; /** Make some properties of a type optional. */ type MakeOptional = Omit & Partial>; /** * @public */ export type FederationRemote = { name: string; entry: string; }; const MANIFEST_PATH = "/mf-manifest.json"; /** Idempotent, so an entry that already names the manifest registers unchanged. */ function withManifest(remote: T): T { if (remote.entry.endsWith(MANIFEST_PATH)) return remote; return { ...remote, entry: new URL(MANIFEST_PATH, remote.entry).href }; } /** * @public */ export type CreateInstanceOptions = MakeOptional< Pick, "remotes" | "plugins" >; /** * @public */ export interface FederationInstance { registerRemotes(remotes: FederationRemote[]): void; /** Load — and evaluate — a remote's expose, returning its module. */ loadRemote(id: string): Promise; /** * Warm a remote's assets (entry + exposes) without evaluating them, so a * later {@link loadRemote} resolves with no network waterfall. `exposes` * (the id after the remote name, e.g. `views/feed/panel`) narrows the warm to * specific exposes; omit it to warm the whole remote. */ preloadRemote(name: string, exposes?: string[]): Promise; } /** * @public */ export function createInstance( options: CreateInstanceOptions, ): FederationInstance { const instance = createMFInstance({ ...options, remotes: options.remotes?.map((remote) => "entry" in remote ? withManifest(remote) : remote, ) ?? [], }); return { registerRemotes: (remotes) => instance.registerRemotes(remotes.map(withManifest), { force: false }), loadRemote: async (id: string) => { let remoteModule: T | null; try { remoteModule = await instance.loadRemote(id); } catch (error) { throw new Error(`Failed to load remote module "${id}"`, { cause: error, }); } return remoteModule; }, // `resourceCategory: "all"` warms async chunks too (a panel's lazy code), // not just the sync entry — the point is a wait-free later load. preloadRemote: (name, exposes) => instance.preloadRemote([ { nameOrAlias: name, exposes, resourceCategory: "all" }, ]), }; } /** * Forward Module Federation lifecycle events to a provided logging function * from workbench. * * @public */ export function log( onDebug: (message: string, context?: Record) => void, ): ModuleFederationRuntimePlugin { const logEvent = (eventName: string, args: unknown) => { // `origin` logs the whole instance, which is noisy const { origin: _origin, ...data } = typeof args === "object" && args !== null ? (args as Record) : { args }; onDebug(`[Lifecycle] ${eventName}`, data); }; return { name: "sanity-logger", beforeInit(args) { logEvent("beforeInit", args); return args; }, beforeRegisterRemote(args) { logEvent("beforeRegisterRemote", args); return args; }, beforePreloadRemote(args) { logEvent("beforePreloadRemote", args); }, beforeRequest(args) { logEvent("beforeRequest", args); return args; }, afterResolve(args) { logEvent("afterResolve", args); return args; }, onLoad(args) { logEvent("onLoad", args); return args; }, loadShare(args) { logEvent("loadShare", args); }, beforeLoadShare(args) { logEvent("beforeLoadShare", args); return args; }, }; }