UNPKG

everything-dev

Version:

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

263 lines (261 loc) • 10.7 kB
import { getCspNonce, getRuntimeConfig } from "./runtime.mjs"; import { createApiClient } from "./api.mjs"; import { createAuthClient } from "./auth.mjs"; import { manifest_exports } from "./manifest/index.mjs"; import { defaultQueryClient } from "./router-defaults.mjs"; import { jsx } from "react/jsx-runtime"; //#region src/ui/hydrate.tsx const mark = (message) => { const progress = window.__CLIENT_PROGRESS__ ?? []; window.__CLIENT_PROGRESS__ = [...progress, message]; if (import.meta.env.DEV) console.log(`[Hydrate] ${message}`); }; function isAbsoluteHttpUrl(value) { if (!value) return false; try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } } /** * Manifest composition: reconstruct the server's tree from the payload — the * same manifests, the same MF names, the same construction code. Digest * parity proves the SSR'd tree and this tree are identical before hydrate; * any failure degrades to the core-only tree (still a real route tree — the * page must render, just without plugin routes) rather than leaving the * router without a tree. */ async function composeFromPayload(runtimeConfig, coreRouteConfig, coreManifest) { const coreUiName = runtimeConfig.ui?.name ?? manifest_exports.CORE_UI_PLUGIN_KEY; const composeCoreOnly = async (candidateManifests) => { try { const core = candidateManifests.find((m) => m.name === manifest_exports.CORE_UI_PLUGIN_KEY); if (!core) return void 0; const tree = await (0, manifest_exports.constructTree)({ name: "core-fallback", plugins: [{ key: core.name, mfName: coreUiName }], resolve: async (ref) => ({ key: ref.key, manifest: core, routeConfig: coreRouteConfig }), rootOptions: coreRouteConfig.rootMeta }); mark(`core-only tree constructed: ${tree.manifests.length} source(s), ${tree.nav.items.length} nav item(s)`); return { routeTree: tree.rootRoute, nav: tree.nav, degraded: true }; } catch (error) { console.error("[Hydrate] Core-only fallback failed:", error); mark(`CORE-ONLY FALLBACK ERROR: ${error.message}`); return; } }; const payload = runtimeConfig.ui?.compose; if (!payload?.remotes) return coreManifest ? composeCoreOnly([coreManifest]) : void 0; const parsed = manifest_exports.ComposePayloadSchema.safeParse(payload); if (!parsed.success) { mark(`compose payload malformed: ${parsed.error.message}`); return coreManifest ? composeCoreOnly([coreManifest]) : void 0; } const { manifests } = parsed.data; const manifestByKey = new Map(manifests.map((m) => [m.name, m])); try { const { registerRemotes, loadRemote } = await import("@module-federation/enhanced/runtime"); mark(`compose payload: digest ${payload.digest}, ${payload.remotes.length} remote(s)`); registerRemotes(payload.remotes.map((remote) => ({ name: remote.name, alias: remote.name, entry: isAbsoluteHttpUrl(remote.manifestUrl) ? remote.manifestUrl : remote.entry.replace(/\/?remoteEntry\.js$/, "/mf-manifest.json") }))); for (const remote of payload.remotes) mark(`remote registered ${remote.name} @ ${remote.entry}`); const loadedRouteConfigs = /* @__PURE__ */ new Map(); const dropped = []; const healthyManifests = []; for (const manifest of manifests) { if (manifest.name === manifest_exports.CORE_UI_PLUGIN_KEY) { healthyManifests.push(manifest); continue; } const remote = payload.remotes.find((r) => r.key === manifest.name); if (!remote) { dropped.push(manifest.name); mark(`dropped ${manifest.name} — payload has no matching remote`); continue; } try { const mod = await loadRemote(`${remote.name}/routeConfig`, { from: "build" }); const routeConfig = mod?.default ?? mod; if (!routeConfig?.routeConfigLoaders) throw new Error(`loadRemote(${remote.name}/routeConfig) returned no route config`); loadedRouteConfigs.set(manifest.name, routeConfig); healthyManifests.push(manifest); mark(`loaded ${remote.name}/routeConfig`); } catch (error) { dropped.push(manifest.name); mark(`dropped ${manifest.name} — ${error.message}`); } } const tree = await (0, manifest_exports.constructTree)({ name: "client", plugins: healthyManifests.map((manifest) => { const remote = payload.remotes.find((r) => r.key === manifest.name); return { key: manifest.name, mfName: remote?.name ?? (manifest.name === manifest_exports.CORE_UI_PLUGIN_KEY ? coreUiName : manifest.name) }; }), resolve: async (ref) => { if (ref.key === manifest_exports.CORE_UI_PLUGIN_KEY) { const manifest = manifestByKey.get(ref.key); if (!manifest) throw new Error(`compose payload has no manifest for "${ref.key}"`); return { key: ref.key, manifest, routeConfig: coreRouteConfig }; } const manifest = manifestByKey.get(ref.key); const routeConfig = loadedRouteConfigs.get(ref.key); if (!manifest || !routeConfig) throw new Error(`composition source "${ref.key}" is not fully resolved`); return { key: ref.key, manifest, routeConfig }; }, rootOptions: coreRouteConfig.rootMeta }); if (dropped.length > 0) { mark(`degraded compose: ${dropped.join(", ")} dropped — client-render`); return { routeTree: tree.rootRoute, nav: tree.nav, degraded: true }; } if (tree.digest !== payload.digest) { console.warn(`[Hydrate] Compose digest mismatch (client ${tree.digest} vs server ${payload.digest}); core-only fallback`); mark(`DIGEST PARITY FAILURE server=${payload.digest} client=${tree.digest}`); return composeCoreOnly(manifests); } mark(`tree constructed: ${tree.manifests.length} source(s), ${tree.nav.items.length} nav item(s)`); return { routeTree: tree.rootRoute, nav: tree.nav, degraded: false }; } catch (error) { console.error("[Hydrate] Client compose failed; core-only fallback:", error); mark(`CLIENT COMPOSE ERROR: ${error.message}`); return composeCoreOnly(manifests); } } function isServerRendered() { if (document.documentElement.hasAttribute("data-everything-ssr")) return true; if (window.__EVERYTHING_DEV_SSR__ !== void 0) return window.__EVERYTHING_DEV_SSR__; return window.$_TSR !== void 0; } function watchHydrationConsumed() { const bootstrapWatch = setInterval(() => { if (window.$_TSR === void 0) { clearInterval(bootstrapWatch); mark("hydration consumed ($_TSR deleted — h() ran)"); } }, 50); } async function hydrate(options) { if (window.__EVERYTHING_DEV_HYDRATE_PROMISE__) return window.__EVERYTHING_DEV_HYDRATE_PROMISE__; const hydratePromise = (async () => { console.log("[Hydrate] Starting..."); const runtimeConfig = options.config ?? getRuntimeConfig(); const cspNonce = getCspNonce(); if (!runtimeConfig.hostUrl || !runtimeConfig.rpcBase) throw new Error("Missing hostUrl or rpcBase in runtime config"); const [{ QueryClientProvider }, routerFactory] = await Promise.all([import("@tanstack/react-query"), options.createRouter ? Promise.resolve(options.createRouter) : import("./router-client.mjs").then((m) => m.createRouter)]); const coreRouteConfig = await options.routeConfig().then((mod) => "default" in mod ? mod.default : mod); const coreManifest = options.manifest ? await options.manifest().then((raw) => typeof raw === "object" && raw !== null && "default" in raw ? raw.default : raw).then((raw) => (0, manifest_exports.parsePluginManifest)(raw)) : void 0; const client = options.createQueryClient?.() ?? defaultQueryClient(); mark(`$_TSR present: ${Boolean(window.$_TSR)}`); const composed = await composeFromPayload(runtimeConfig, coreRouteConfig, coreManifest); const { router, queryClient: builtClient } = routerFactory({ routeTree: composed?.routeTree, context: { pluginNav: composed?.nav, queryClient: client, runtimeConfig, cspNonce, locale: isServerRendered() ? document.documentElement.lang : void 0, apiClient: createApiClient({ hostUrl: runtimeConfig.hostUrl, rpcBase: runtimeConfig.rpcBase, connectionError: options.apiConnectionError }), authClient: createAuthClient({ runtimeConfig, cspNonce }) } }); const providerClient = builtClient ?? client; if (isServerRendered() && Boolean(composed) && !composed.degraded) { const { hydrateRoot } = await import("react-dom/client"); const { RouterClient } = await import("@tanstack/react-router/ssr/client"); console.log("[Hydrate] Calling hydrateRoot..."); hydrateRoot(document, /* @__PURE__ */ jsx(QueryClientProvider, { client: providerClient, children: /* @__PURE__ */ jsx(RouterClient, { router }) })); watchHydrationConsumed(); } else { const { createRoot } = await import("react-dom/client"); const { RouterProvider } = await import("@tanstack/react-router"); console.log("[Hydrate] Calling createRoot..."); createRoot(document).render(/* @__PURE__ */ jsx(QueryClientProvider, { client: providerClient, children: /* @__PURE__ */ jsx(RouterProvider, { router }) })); } if (composed?.degraded) { const { toast } = await import("sonner"); toast.error("Some application features couldn't load", { id: "application-compose-error", description: "Plugin pages are unavailable. Reload to try again.", duration: Number.POSITIVE_INFINITY, action: { label: "Reload", onClick: () => window.location.reload() } }); } console.log("[Hydrate] Complete!"); })().catch((error) => { console.error("[Hydrate] Failed:", error); window.__EVERYTHING_DEV_HYDRATE_PROMISE__ = void 0; const message = document.createElement("main"); message.setAttribute("role", "alert"); message.dataset.testid = "application-startup-error"; message.className = "min-h-screen flex flex-col items-center justify-center gap-4 bg-background text-foreground p-6"; const heading = document.createElement("h1"); heading.textContent = "The application couldn't load"; const detail = document.createElement("p"); detail.textContent = "Check your connection and reload to try again."; const retry = document.createElement("button"); retry.textContent = "Reload"; retry.className = "rounded-md border border-border px-4 py-2"; retry.addEventListener("click", () => window.location.reload()); message.append(heading, detail, retry); document.body.replaceChildren(message); throw error; }); window.__EVERYTHING_DEV_HYDRATE_PROMISE__ = hydratePromise; return hydratePromise; } //#endregion export { hydrate as default, hydrate }; //# sourceMappingURL=hydrate.mjs.map