UNPKG

everything-dev

Version:

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

392 lines (390 loc) • 14.6 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_http_client = require('../../utils/http-client.cjs'); const require_services_registry_bundle_cache = require('../registry/bundle-cache.cjs'); const require_services_deploy_integrity = require('../deploy/integrity.cjs'); let node_fs = require("node:fs"); let _module_federation_enhanced_runtime = require("@module-federation/enhanced/runtime"); let _module_federation_runtime_core = require("@module-federation/runtime-core"); let every_plugin_build_rspack = require("every-plugin/build/rspack"); //#region src/services/federation/service.ts let mfInstance = null; function patchManifestFetchForSsrPublicPath(mf) { if (!mf || !mf.loaderHook?.lifecycle?.fetch?.on) return; if (mf.__everythingDevPatchedManifestFetch === true) return; mf.__everythingDevPatchedManifestFetch = true; mf.loaderHook.lifecycle.fetch.on((url, init) => { if (typeof url !== "string" || !url.endsWith("/mf-manifest.json")) return; return require_http_client.fetchRaw(url, init, "15 seconds").then((res) => res.json()).then((json) => { json.metaData = json.metaData ?? {}; json.metaData.ssrPublicPath = json.metaData.ssrPublicPath ?? url.replace(/\/mf-manifest\.json$/, "/"); if (json.metaData.publicPath === "auto" || json.metaData.publicPath === "") json.metaData.publicPath = "/"; return new Response(JSON.stringify(json), { headers: { "content-type": "application/json" } }); }); }); } function installIntegrityFetchHook(mf, registry, options) { if (!mf || !mf.loaderHook?.lifecycle?.fetch?.on) { console.warn("[SRI] MF lifecycle fetch hook not available, skipping integrity-in-pipeline"); return; } if (mf.__everythingDevIntegrityHook === true) return; mf.__everythingDevIntegrityHook = true; mf.loaderHook.lifecycle.fetch.on((url, init) => { if (typeof url !== "string") return; const expectedHash = registry.get(url); if (!expectedHash) return; return require_http_client.fetchRaw(url, init, "15 seconds").then(async (res) => { const buffer = Buffer.from(await res.arrayBuffer()); const computed = require_services_deploy_integrity.computeSriHash(buffer); if (computed !== expectedHash) { const cached = await require_services_registry_bundle_cache.readBundleCache(url, { cacheDir: options?.cacheDir }); if (cached && require_services_deploy_integrity.computeSriHash(Buffer.from(cached)) === expectedHash) { console.warn(`[SRI] Origin bytes failed verification for ${url} — serving last-known-good from the bundle cache`); return new Response(cached.slice().buffer, { status: 200, headers: { "x-bundle-cache": "stale" } }); } console.error(`[SRI] Integrity check failed in MF fetch pipeline for ${url}\n Expected: ${expectedHash}\n Computed: ${computed}`); return new Response(`Integrity check failed for ${url}`, { status: 500, statusText: "Integrity Check Failed" }); } await require_services_registry_bundle_cache.writeBundleCache(url, new Uint8Array(buffer), { cacheDir: options?.cacheDir }).catch(() => {}); return new Response(buffer, { status: res.status, statusText: res.statusText, headers: res.headers }); }); }); } function getFederationInstance() { if (mfInstance) return mfInstance; const existing = (0, _module_federation_enhanced_runtime.getInstance)(); if (existing) { mfInstance = existing; (0, _module_federation_runtime_core.setGlobalFederationInstance)(mfInstance); patchManifestFetchForSsrPublicPath(mfInstance); return mfInstance; } mfInstance = (0, _module_federation_enhanced_runtime.createInstance)({ name: "host", remotes: [] }); (0, _module_federation_runtime_core.setGlobalFederationInstance)(mfInstance); patchManifestFetchForSsrPublicPath(mfInstance); return mfInstance; } async function registerRemote(opts) { const instance = getFederationInstance(); const inferType = () => { if (opts.type) return opts.type; if (opts.entry.endsWith("/mf-manifest.json")) return "manifest"; if (/\/remoteEntry(\.[a-f0-9]{8,})?\.js$/.test(opts.entry)) return "script"; return typeof window === "undefined" ? "script" : "manifest"; }; const remoteType = inferType(); instance.registerRemotes([{ name: opts.name, entry: opts.entry, type: remoteType }]); } async function loadRemoteModule(specifier, options) { const instance = getFederationInstance(); if (typeof window === "undefined") await instance.initializeSharing?.("default"); const mod = await instance.loadRemote(specifier, options); if (!mod) throw new Error(`Failed to load remote module: ${specifier}`); return mod; } async function ensureNodeRuntimePlugin() { const instance = getFederationInstance(); if (typeof window !== "undefined") return; if (instance.__nodeRuntimePluginLoaded) return; const mod = await import("@module-federation/node/runtimePlugin"); const factory = mod?.default ?? mod; const plugin = typeof factory === "function" ? factory() : null; if (plugin) instance.registerPlugins([plugin]); instance.__nodeRuntimePluginLoaded = true; } function parseSemver(v) { const m = typeof v === "string" ? /^(\d+)\.(\d+)\.(\d+)/.exec(v) : null; return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null; } function satisfiesConstraint(version, constraint) { const trimmed = constraint.trim(); if (!trimmed) return true; if (![ "^", "~", ">", "<", "=", "x" ].some((c) => trimmed.startsWith(c))) return version === trimmed; const m = /^\s*\^\s*(\d+)\.(\d+)\.(\d+)/.exec(trimmed); if (!m) return true; const v = parseSemver(version); if (!v) return false; if (v.major !== +m[1]) return false; if (v.minor < +m[2] || v.minor === +m[2] && v.patch < +m[3]) return false; const constraintPrerelease = trimmed.match(/-([0-9A-Za-z.-]+)/); const versionPrerelease = /-([0-9A-Za-z.-]+)\s*$/.exec(version); if (constraintPrerelease) { if (!versionPrerelease) return false; const cPre = constraintPrerelease[1].split("."); const vPre = versionPrerelease[1].split("."); for (let i = 0; i < Math.max(cPre.length, vPre.length); i += 1) { const c = cPre[i] ?? "0"; const p = vPre[i] ?? "0"; if (c === p) continue; const cNum = /^\d+$/.test(c) ? +c : null; const pNum = /^\d+$/.test(p) ? +p : null; if (cNum !== null && pNum !== null) return cNum < pNum; return cNum === null ? c.localeCompare(p) < 0 : false; } return true; } if (versionPrerelease) return false; return true; } function manifestUrl(remoteUrl) { return remoteUrl.endsWith("/") ? `${remoteUrl}mf-manifest.json` : `${remoteUrl}/mf-manifest.json`; } async function fetchManifest(url, timeoutMs) { const target = url.endsWith(".json") ? url : manifestUrl(url); const res = await require_http_client.fetchRaw(target, {}, timeoutMs); if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); return res.json(); } function listFederationRemotes(bosConfig) { const out = []; for (const role of ["api", "auth"]) { const url = readProductionUrl((bosConfig?.app)?.[role]); if (url) out.push({ role, url }); } for (const [id, slot] of Object.entries(bosConfig?.plugins ?? {})) { const url = readProductionUrl(slot); if (url) out.push({ role: id, url }); } return out; } /** * The UI composition compat check (#546) — mf check's mirror of the host's * boot guard. The core UI remote is the composition's shared-singleton * provider; plugin UI remotes are consumers. Any singleton named by two or * more UI manifests at different versions fails the composition at * `mf.loadRemote` with the federation runtime's cryptic negotiation error — * this names the slots and versions instead. Server remotes (api/auth) are * intentionally out of scope: the host-side `checkFederationCompat` covers * them against the host's own core map. */ async function checkUiCompositionCompat(bosConfig, opts = {}) { const timeoutMs = opts.timeoutMs ?? 15e3; const uiSlots = []; const pushSlot = (slot, entry) => { const asRef = entry; const url = typeof asRef?.browserManifestUrl === "string" && asRef.browserManifestUrl ? asRef.browserManifestUrl : readProductionUrl(entry); if (url) uiSlots.push({ slot, url }); }; pushSlot("app.ui", (bosConfig?.app)?.ui); for (const [id, slot] of Object.entries(bosConfig?.plugins ?? {})) { if (!slot || typeof slot === "string") continue; const ui = slot.ui; if (ui) pushSlot(`plugins.${id}.ui`, ui); } if (uiSlots.length === 0) return { ok: true, slots: [] }; const resolved = await Promise.all(uiSlots.map(async ({ slot, url }) => { try { return { slot, url, reachable: true, shared: (await fetchManifest(url, timeoutMs)).shared ?? [] }; } catch (e) { return { slot, url, reachable: false, shared: [], reason: e instanceof Error ? e.message : String(e) }; } })); const byName = /* @__PURE__ */ new Map(); for (const entry of resolved) for (const dep of entry.shared) { if (typeof dep.version !== "string") continue; const uses = byName.get(dep.name) ?? []; uses.push({ slot: entry.slot, version: dep.version }); byName.set(dep.name, uses); } const slots = resolved.map((entry) => { if (!entry.reachable) return { slot: entry.slot, url: uiSlots.find((s) => s.slot === entry.slot).url, reachable: false, ok: false, reason: entry.reason }; const skewed = [...byName.entries()].flatMap(([name, uses]) => { const first = uses[0]; return uses.find((use) => use.version !== first.version) ? [name] : []; }); if (skewed.length === 0) return { slot: entry.slot, url: uiSlots.find((s) => s.slot === entry.slot).url, reachable: true, ok: true }; return { slot: entry.slot, url: uiSlots.find((s) => s.slot === entry.slot).url, reachable: true, ok: false, reason: `shared singleton skew across UI slots: ${skewed.map((name) => { return `${name} is ${byName.get(name).map((use) => `${use.slot}=${use.version}`).join(", ")}`; }).join("; ")}` }; }); return { ok: slots.every((slot) => slot.ok), slots }; } function readProductionUrl(entry) { if (typeof entry === "string") return entry.length > 0 ? entry : null; if (entry && typeof entry.production === "string") { const url = entry.production; return url.length > 0 ? url : null; } return null; } async function checkFederationCompat(bosConfig, opts = {}) { const timeoutMs = opts.timeoutMs ?? 15e3; if (!bosConfig?.app?.host?.production) return { ok: true, hostVersion: null, hostReachable: false, remotes: [] }; const hostUrl = bosConfig.app.host.production; let hostManifest = null; let hostReason; try { hostManifest = await fetchManifest(hostUrl, timeoutMs); } catch (e) { hostReason = e instanceof Error ? e.message : String(e); } const hostVersion = hostManifest?.metaData?.pluginVersion ?? null; const hostShared = (hostManifest?.shared ?? []).filter((s) => typeof s.requiredVersion === "string"); const remotes = listFederationRemotes(bosConfig); const remoteReports = (await Promise.allSettled(remotes.map(async ({ role, url }) => { let manifest = null; let reason; try { manifest = await fetchManifest(url, timeoutMs); } catch (e) { reason = e instanceof Error ? e.message : String(e); } const version = manifest?.metaData?.pluginVersion ?? null; const versionOk = version !== null && version === hostVersion; if (!manifest) return { role, url, reachable: false, pluginVersion: null, ok: false, reason: reason ?? "unreachable" }; if (!versionOk) return { role, url, reachable: true, pluginVersion: version, ok: false, reason: hostVersion ? `expected pluginVersion=${hostVersion} (host), got ${version ?? "missing"} — redeploy this plugin` : `host manifest missing pluginVersion` }; const missingShared = []; const mismatchedShared = []; for (const hostDep of hostShared) { const remDep = manifest.shared?.find((s) => s.name === hostDep.name); if (!remDep) { missingShared.push(`${hostDep.name}@${hostDep.requiredVersion}`); continue; } if (!satisfiesConstraint(remDep.version ?? "", hostDep.requiredVersion)) mismatchedShared.push(`${hostDep.name}: remote=${remDep.version ?? "?"} hostReq=${hostDep.requiredVersion}`); if ((0, every_plugin_build_rspack.isEffectCriticalSharedDep)(hostDep.name)) { const hostVersion = hostDep.version ?? ""; const remoteVersion = remDep.version ?? ""; if (remoteVersion && remoteVersion !== hostVersion) mismatchedShared.push(`${hostDep.name}: exact-version skew — remote=${remoteVersion} host=${hostVersion} — bundle was built against a different prerelease build; redeploy host and plugin together`); const remoteRequired = typeof remDep.requiredVersion === "string" ? remDep.requiredVersion.trim() : ""; if (remoteRequired.length > 0 && remoteRequired[0] !== "^" && !satisfiesConstraint(hostVersion, remoteRequired)) mismatchedShared.push(`${hostDep.name}: train skew — host=${hostVersion} vs remote requiredVersion=${remoteRequired}`); } } const reasons = [...missingShared.map((d) => `missing shared "${d}" required by host`), ...mismatchedShared]; return { role, url, reachable: true, pluginVersion: version, ok: reasons.length === 0, reason: reasons[0] }; }))).map((s, i) => { const placeholder = { role: remotes[i].role, url: remotes[i].url, reachable: false, pluginVersion: null, ok: false }; return s.status === "fulfilled" ? s.value : { ...placeholder, reason: String(s.reason) }; }); return { ok: hostManifest !== null && remoteReports.every((r) => r.ok && r.reachable && r.pluginVersion !== null), hostVersion, hostReachable: hostManifest !== null, hostReason, remotes: remoteReports }; } function loadBosConfigForMfCheck(configPath) { return JSON.parse((0, node_fs.readFileSync)(configPath, "utf8")); } //#endregion exports.checkFederationCompat = checkFederationCompat; exports.checkUiCompositionCompat = checkUiCompositionCompat; exports.ensureNodeRuntimePlugin = ensureNodeRuntimePlugin; exports.getFederationInstance = getFederationInstance; exports.installIntegrityFetchHook = installIntegrityFetchHook; exports.loadBosConfigForMfCheck = loadBosConfigForMfCheck; exports.loadRemoteModule = loadRemoteModule; exports.patchManifestFetchForSsrPublicPath = patchManifestFetchForSsrPublicPath; exports.registerRemote = registerRemote; exports.satisfiesConstraint = satisfiesConstraint; //# sourceMappingURL=service.cjs.map