@tanstack/router-core
Version:
Modern and scalable routing for React applications
176 lines (175 loc) • 6.24 kB
JavaScript
import { batch } from "../utils/batch.js";
import { createControlledPromise } from "../utils.js";
import { isNotFound } from "../not-found.js";
import { hydrateSsrMatchId } from "./ssr-match-id.js";
import invariant from "tiny-invariant";
//#region src/ssr/ssr-client.ts
function hydrateMatch(match, deyhydratedMatch) {
match.id = deyhydratedMatch.i;
match.__beforeLoadContext = deyhydratedMatch.b;
match.loaderData = deyhydratedMatch.l;
match.status = deyhydratedMatch.s;
match.ssr = deyhydratedMatch.ssr;
match.updatedAt = deyhydratedMatch.u;
match.error = deyhydratedMatch.e;
if (deyhydratedMatch.g !== void 0) match.globalNotFound = deyhydratedMatch.g;
}
async function hydrate(router) {
invariant(window.$_TSR, "Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!");
const serializationAdapters = router.options.serializationAdapters;
if (serializationAdapters?.length) {
const fromSerializableMap = /* @__PURE__ */ new Map();
serializationAdapters.forEach((adapter) => {
fromSerializableMap.set(adapter.key, adapter.fromSerializable);
});
window.$_TSR.t = fromSerializableMap;
window.$_TSR.buffer.forEach((script) => script());
}
window.$_TSR.initialized = true;
invariant(window.$_TSR.router, "Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!");
const dehydratedRouter = window.$_TSR.router;
dehydratedRouter.matches.forEach((dehydratedMatch) => {
dehydratedMatch.i = hydrateSsrMatchId(dehydratedMatch.i);
});
if (dehydratedRouter.lastMatchId) dehydratedRouter.lastMatchId = hydrateSsrMatchId(dehydratedRouter.lastMatchId);
const { manifest, dehydratedData, lastMatchId } = dehydratedRouter;
router.ssr = { manifest };
const nonce = document.querySelector("meta[property=\"csp-nonce\"]")?.content;
router.options.ssr = { nonce };
const matches = router.matchRoutes(router.state.location);
const routeChunkPromise = Promise.all(matches.map((match) => router.loadRouteChunk(router.looseRoutesById[match.routeId])));
function setMatchForcePending(match) {
const pendingMinMs = router.looseRoutesById[match.routeId].options.pendingMinMs ?? router.options.defaultPendingMinMs;
if (pendingMinMs) {
const minPendingPromise = createControlledPromise();
match._nonReactive.minPendingPromise = minPendingPromise;
match._forcePending = true;
setTimeout(() => {
minPendingPromise.resolve();
router.updateMatch(match.id, (prev) => {
prev._nonReactive.minPendingPromise = void 0;
return {
...prev,
_forcePending: void 0
};
});
}, pendingMinMs);
}
}
function setRouteSsr(match) {
const route = router.looseRoutesById[match.routeId];
if (route) route.options.ssr = match.ssr;
}
let firstNonSsrMatchIndex = void 0;
matches.forEach((match) => {
const dehydratedMatch = dehydratedRouter.matches.find((d) => d.i === match.id);
if (!dehydratedMatch) {
match._nonReactive.dehydrated = false;
match.ssr = false;
setRouteSsr(match);
return;
}
hydrateMatch(match, dehydratedMatch);
setRouteSsr(match);
match._nonReactive.dehydrated = match.ssr !== false;
if (match.ssr === "data-only" || match.ssr === false) {
if (firstNonSsrMatchIndex === void 0) {
firstNonSsrMatchIndex = match.index;
setMatchForcePending(match);
}
}
});
router.__store.setState((s) => ({
...s,
matches
}));
await router.options.hydrate?.(dehydratedData);
await Promise.all(router.state.matches.map(async (match) => {
try {
const route = router.looseRoutesById[match.routeId];
const parentContext = router.state.matches[match.index - 1]?.context ?? router.options.context;
if (route.options.context) {
const contextFnContext = {
deps: match.loaderDeps,
params: match.params,
context: parentContext ?? {},
location: router.state.location,
navigate: (opts) => router.navigate({
...opts,
_fromLocation: router.state.location
}),
buildLocation: router.buildLocation,
cause: match.cause,
abortController: match.abortController,
preload: false,
matches,
routeId: route.id
};
match.__routeContext = route.options.context(contextFnContext) ?? void 0;
}
match.context = {
...parentContext,
...match.__routeContext,
...match.__beforeLoadContext
};
const assetContext = {
ssr: router.options.ssr,
matches: router.state.matches,
match,
params: match.params,
loaderData: match.loaderData
};
const headFnContent = await route.options.head?.(assetContext);
const scripts = await route.options.scripts?.(assetContext);
match.meta = headFnContent?.meta;
match.links = headFnContent?.links;
match.headScripts = headFnContent?.scripts;
match.styles = headFnContent?.styles;
match.scripts = scripts;
} catch (err) {
if (isNotFound(err)) {
match.error = { isNotFound: true };
console.error(`NotFound error during hydration for routeId: ${match.routeId}`, err);
} else {
match.error = err;
console.error(`Error during hydration for route ${match.routeId}:`, err);
throw err;
}
}
}));
const isSpaMode = matches[matches.length - 1].id !== lastMatchId;
if (!matches.some((m) => m.ssr === false) && !isSpaMode) {
matches.forEach((match) => {
match._nonReactive.dehydrated = void 0;
});
return routeChunkPromise;
}
const loadPromise = Promise.resolve().then(() => router.load()).catch((err) => {
console.error("Error during router hydration:", err);
});
if (isSpaMode) {
const match = matches[1];
invariant(match, "Expected to find a match below the root match in SPA mode.");
setMatchForcePending(match);
match._displayPending = true;
match._nonReactive.displayPendingPromise = loadPromise;
loadPromise.then(() => {
batch(() => {
if (router.__store.state.status === "pending") router.__store.setState((s) => ({
...s,
status: "idle",
resolvedLocation: s.location
}));
router.updateMatch(match.id, (prev) => ({
...prev,
_displayPending: void 0,
displayPendingPromise: void 0
}));
});
});
}
return routeChunkPromise;
}
//#endregion
export { hydrate };
//# sourceMappingURL=ssr-client.js.map