UNPKG

everything-dev

Version:

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

296 lines (294 loc) • 11 kB
import { readBundleCache, writeBundleCache } from "./bundle-cache.mjs"; import { computeSriHash } from "./integrity.mjs"; import { readFileSync } from "node:fs"; import { createInstance, getInstance } from "@module-federation/enhanced/runtime"; import { setGlobalFederationInstance } from "@module-federation/runtime-core"; import { isEffectCriticalSharedDep } from "every-plugin/build/rspack"; //#region src/mf.ts let mfInstance = null; function fetchWithTimeout(url, init, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer)); } 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 fetchWithTimeout(url, init, 15e3).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 fetchWithTimeout(url, init, 15e3).then(async (res) => { const buffer = Buffer.from(await res.arrayBuffer()); const computed = computeSriHash(buffer); if (computed !== expectedHash) { const cached = await readBundleCache(url, { cacheDir: options?.cacheDir }); if (cached && 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 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 = getInstance(); if (existing) { mfInstance = existing; setGlobalFederationInstance(mfInstance); patchManifestFetchForSsrPublicPath(mfInstance); return mfInstance; } mfInstance = createInstance({ name: "host", remotes: [] }); 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 res = await fetch(manifestUrl(url), { signal: AbortSignal.timeout(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; } 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 (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(readFileSync(configPath, "utf8")); } //#endregion export { checkFederationCompat, ensureNodeRuntimePlugin, getFederationInstance, installIntegrityFetchHook, loadBosConfigForMfCheck, loadRemoteModule, patchManifestFetchForSsrPublicPath, registerRemote }; //# sourceMappingURL=mf.mjs.map