@tanstack/router-core
Version:
Modern and scalable routing for React applications
499 lines (498 loc) • 17.8 kB
JavaScript
const require_utils = require("../utils.cjs");
const require_invariant = require("../invariant.cjs");
const require_lru_cache = require("../lru-cache.cjs");
const require_root = require("../root.cjs");
const require_manifest = require("../manifest.cjs");
const require_constants = require("./constants.cjs");
const require_transformer = require("./serializer/transformer.cjs");
const require_seroval_plugins = require("./serializer/seroval-plugins.cjs");
const require_ssr_match_id = require("./ssr-match-id.cjs");
const require_tsrScript = require("./tsrScript.cjs");
let seroval = require("seroval");
//#region src/ssr/ssr-server.ts
const SCOPE_ID = "tsr";
const TSR_PREFIX = require_constants.GLOBAL_TSR + ".router=";
const P_PREFIX = require_constants.GLOBAL_TSR + ".p(()=>";
const P_SUFFIX = ")";
function dehydrateMatch(match) {
const dehydratedMatch = {
i: require_ssr_match_id.dehydrateSsrMatchId(match.id),
u: match.updatedAt,
s: match.status
};
for (const [key, shorthand] of [
["__beforeLoadContext", "b"],
["loaderData", "l"],
["error", "e"],
["ssr", "ssr"]
]) if (match[key] !== void 0) dehydratedMatch[shorthand] = match[key];
if (match.globalNotFound) dehydratedMatch.g = true;
return dehydratedMatch;
}
const INITIAL_SCRIPTS = [(0, seroval.getCrossReferenceHeader)(SCOPE_ID), require_tsrScript.default];
var ScriptBuffer = class {
constructor(injectScript) {
this._scriptBarrierLifted = false;
this._cleanedUp = false;
this._microtaskVersion = 0;
this._pendingMicrotaskVersion = 0;
this.injectScript = injectScript;
this._queue = INITIAL_SCRIPTS.slice();
}
enqueue(script) {
if (this._cleanedUp) return;
this._queue.push(script);
if (this._scriptBarrierLifted) this.scheduleInjectBufferedScripts();
}
liftBarrier() {
if (this._scriptBarrierLifted || this._cleanedUp) return;
this._scriptBarrierLifted = true;
if (this._queue.length > 0) this.scheduleInjectBufferedScripts();
}
scheduleInjectBufferedScripts() {
if (this._pendingMicrotaskVersion !== 0) return;
const pendingVersion = ++this._microtaskVersion;
this._pendingMicrotaskVersion = pendingVersion;
queueMicrotask(() => {
if (this._pendingMicrotaskVersion !== pendingVersion) return;
this._pendingMicrotaskVersion = 0;
this.injectBufferedScripts();
});
}
clearPendingMicrotask() {
if (this._pendingMicrotaskVersion === 0) return;
this._pendingMicrotaskVersion = 0;
this._microtaskVersion++;
}
/**
* Flushes any pending scripts synchronously.
* Call this before signaling serialization finished to ensure all scripts are injected.
*
* IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,
* scripts should remain in the queue so takeBufferedScripts() can retrieve them
*/
flush() {
if (!this._scriptBarrierLifted) return;
if (this._cleanedUp) return;
this.clearPendingMicrotask();
this.injectBufferedScripts();
}
takeAll() {
return this.takeScripts(this._queue.length);
}
takeScripts(count) {
if (count <= 0) return void 0;
const bufferedScripts = this._queue.splice(0, count);
if (bufferedScripts.length === 0) return;
if (bufferedScripts.length === 1) return bufferedScripts[0] + ";document.currentScript.remove()";
return bufferedScripts.join(";") + ";document.currentScript.remove()";
}
hasPending() {
return this._queue.length > 0;
}
injectBufferedScripts() {
if (this._cleanedUp) return;
if (this._queue.length === 0) return;
const scriptsToInject = this.takeAll();
if (scriptsToInject) this.injectScript?.(scriptsToInject);
}
cleanup() {
this._cleanedUp = true;
this.clearPendingMicrotask();
this._queue = [];
this.injectScript = void 0;
}
};
const isProd = process.env.NODE_ENV === "production";
const MANIFEST_CACHE_SIZE = 100;
const manifestCaches = /* @__PURE__ */ new WeakMap();
function getManifestCache(manifest) {
const cache = manifestCaches.get(manifest);
if (cache) return cache;
const newCache = require_lru_cache.createLRUCache(MANIFEST_CACHE_SIZE);
manifestCaches.set(manifest, newCache);
return newCache;
}
function getInlineCssForPreparedRoutes(manifest, preparedRoutes) {
if (preparedRoutes.inlineCss !== void 0) return preparedRoutes.inlineCss;
const styles = manifest.inlineCss?.styles;
const hrefs = preparedRoutes.inlineCssHrefs;
if (!styles || !hrefs?.length) return void 0;
let css = "";
for (const href of hrefs) css += styles[href];
preparedRoutes.inlineCss = css;
return css;
}
function getInlineCssAssetForPreparedRoutes(manifest, preparedRoutes) {
const css = getInlineCssForPreparedRoutes(manifest, preparedRoutes);
return css === void 0 ? void 0 : require_manifest.createInlineCssStyleAsset(css);
}
function getMatchedRoutesCacheKey(matches) {
let cacheKey = "";
for (let i = 0; i < matches.length; i++) cacheKey += (i === 0 ? "" : "\0") + matches[i].routeId;
return cacheKey;
}
function getPreparedMatchedManifestRoutes(manifest, matches, cacheKey) {
if (isProd) {
const cached = getManifestCache(manifest).get(cacheKey);
if (cached) return cached;
}
const preparedRoutes = prepareMatchedManifestRoutes(manifest, matches);
if (isProd) getManifestCache(manifest).set(cacheKey, preparedRoutes);
return preparedRoutes;
}
function prepareMatchedManifestRoutes(manifest, matches) {
const inlineStyles = manifest.inlineCss?.styles;
const routes = {};
if (!inlineStyles) {
for (const match of matches) {
const route = manifest.routes[match.routeId];
if (route) routes[match.routeId] = route;
}
return {
routes,
hasStrippedRoutes: false
};
}
const inlineCssHrefs = [];
const seenInlineCssHrefs = /* @__PURE__ */ new Set();
let hasStrippedRoutes = false;
for (const match of matches) {
const routeId = match.routeId;
const route = manifest.routes[routeId];
if (!route) continue;
const nextRoute = stripInlinedStylesheetAssetsFromRoute(inlineStyles, route, inlineCssHrefs, seenInlineCssHrefs);
if (nextRoute !== route) hasStrippedRoutes = true;
routes[routeId] = nextRoute;
}
return {
routes,
hasStrippedRoutes,
...inlineCssHrefs.length ? { inlineCssHrefs } : {}
};
}
function stripInlinedStylesheetAssetsFromRoute(inlineStyles, route, inlineCssHrefs, seenInlineCssHrefs) {
const css = route.css;
if (!css) return route;
if (css.length === 0) {
const nextRoute = { ...route };
delete nextRoute.css;
return nextRoute;
}
let cssLinks;
for (let i = 0; i < css.length; i++) {
const link = css[i];
const href = require_manifest.getStylesheetHref(link);
if (inlineStyles[href] === void 0) {
if (cssLinks) cssLinks.push(link);
continue;
}
if (!seenInlineCssHrefs.has(href)) {
seenInlineCssHrefs.add(href);
inlineCssHrefs.push(href);
}
if (!cssLinks) cssLinks = css.slice(0, i);
}
if (!cssLinks) return route;
if (cssLinks.length > 0) return {
...route,
css: cssLinks
};
const nextRoute = { ...route };
delete nextRoute.css;
return nextRoute;
}
function hasRouteAssets(route) {
return !!route.scripts?.length || !!route.css?.length;
}
function hasRequestAssets(assets) {
return !!assets && (!!assets.preloads?.length || hasRouteAssets(assets));
}
function mergeRequestAssetsIntoRootRoute(rootRoute, requestAssets) {
const preloads = requestAssets?.preloads?.length ? [...requestAssets.preloads, ...rootRoute?.preloads ?? []] : rootRoute?.preloads;
const scripts = requestAssets?.scripts?.length ? [...requestAssets.scripts, ...rootRoute?.scripts ?? []] : rootRoute?.scripts;
const cssLinks = requestAssets?.css?.length ? [...requestAssets.css, ...rootRoute?.css ?? []] : rootRoute?.css;
return {
...rootRoute ?? {},
...preloads?.length ? { preloads } : {},
...scripts?.length ? { scripts } : {},
...cssLinks?.length ? { css: cssLinks } : {}
};
}
function attachRouterServerSsrUtils({ router, manifest, getRequestAssets }) {
router.ssr = { get manifest() {
if (!manifest) return manifest;
const requestAssets = getRequestAssets?.();
const matches = router.stores.matches.get();
const hasAssets = hasRequestAssets(requestAssets);
if (!hasAssets && !manifest.inlineCss) return manifest;
let inlineCssAsset;
let routes = manifest.routes;
if (manifest.inlineCss) {
const preparedManifest = getPreparedMatchedManifestRoutes(manifest, matches, getMatchedRoutesCacheKey(matches));
inlineCssAsset = getInlineCssAssetForPreparedRoutes(manifest, preparedManifest);
if (preparedManifest.hasStrippedRoutes) routes = {
...manifest.routes,
...preparedManifest.routes
};
}
if (!hasAssets) return {
...manifest.scriptFormat ? { scriptFormat: manifest.scriptFormat } : {},
...inlineCssAsset ? { inlineStyle: inlineCssAsset } : {},
routes
};
const rootRoute = routes[require_root.rootRouteId];
return {
...manifest.scriptFormat ? { scriptFormat: manifest.scriptFormat } : {},
...inlineCssAsset ? { inlineStyle: inlineCssAsset } : {},
routes: {
...routes,
[require_root.rootRouteId]: mergeRequestAssetsIntoRootRoute(rootRoute, requestAssets)
}
};
} };
let _dehydrated = false;
let _serializationFinished = false;
let streamFastPathReserved = false;
const renderFinishedListeners = [];
const injectedHtmlListeners = [];
const serializationFinishedListeners = [];
const cleanupListeners = [];
let cleanupStarted = false;
let injectedHtmlBuffer = "";
const callListeners = (listeners, errorPrefix) => {
const snapshot = listeners.slice();
for (const l of snapshot) try {
l();
} catch (err) {
console.error(`${errorPrefix}:`, err);
}
};
const removeListener = (listeners, listener) => {
const index = listeners.indexOf(listener);
if (index >= 0) listeners.splice(index, 1);
};
const scriptBuffer = new ScriptBuffer((script) => {
serverSsr.injectScript(script);
});
const serverSsr = {
injectHtml: (html) => {
if (!html || cleanupStarted) return;
injectedHtmlBuffer += html;
callListeners(injectedHtmlListeners, "SSR injected HTML listener error");
},
injectScript: (script) => {
if (!script || cleanupStarted) return;
const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ""}>${script}<\/script>`;
serverSsr.injectHtml(html);
},
dehydrate: async (opts) => {
if (_dehydrated) {
if (process.env.NODE_ENV !== "production") throw new Error("Invariant failed: router is already dehydrated!");
require_invariant.invariant();
}
let matchesToDehydrate = router.stores.matches.get();
if (router.isShell()) matchesToDehydrate = matchesToDehydrate.slice(0, 1);
const matches = matchesToDehydrate.map(dehydrateMatch);
let manifestToDehydrate = void 0;
if (manifest) {
const cacheKey = getMatchedRoutesCacheKey(matchesToDehydrate);
const preparedManifest = getPreparedMatchedManifestRoutes(manifest, matchesToDehydrate, cacheKey);
manifestToDehydrate = {
...manifest.scriptFormat ? { scriptFormat: manifest.scriptFormat } : {},
...preparedManifest.inlineCssHrefs ? { inlineStyle: require_manifest.createInlineCssPlaceholderAsset() } : {},
routes: preparedManifest.routes
};
const requestAssets = opts?.requestAssets;
if (hasRequestAssets(requestAssets)) {
const existingRoot = manifestToDehydrate.routes[require_root.rootRouteId];
manifestToDehydrate.routes = {
...manifestToDehydrate.routes,
[require_root.rootRouteId]: mergeRequestAssetsIntoRootRoute(existingRoot, requestAssets)
};
}
}
const dehydratedRouter = {
manifest: manifestToDehydrate,
matches
};
const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id;
if (lastMatchId) dehydratedRouter.lastMatchId = require_ssr_match_id.dehydrateSsrMatchId(lastMatchId);
const dehydratedData = await router.options.dehydrate?.();
if (dehydratedData) dehydratedRouter.dehydratedData = dehydratedData;
_dehydrated = true;
const trackPlugins = { didRun: false };
const serializationAdapters = router.options.serializationAdapters;
const plugins = serializationAdapters ? serializationAdapters.map((t) => /* @__PURE__ */ require_transformer.makeSsrSerovalPlugin(t, trackPlugins)).concat(require_seroval_plugins.defaultSerovalPlugins) : require_seroval_plugins.defaultSerovalPlugins;
let serializationCompleteSignaled = false;
const signalSerializationComplete = () => {
if (serializationCompleteSignaled || cleanupStarted) return;
serializationCompleteSignaled = true;
_serializationFinished = true;
const listeners = serializationFinishedListeners.slice();
serializationFinishedListeners.length = 0;
for (const l of listeners) try {
l();
} catch (err) {
console.error("Serialization listener error:", err);
}
};
const finishScriptSerialization = () => {
if (serializationCompleteSignaled || cleanupStarted) return;
scriptBuffer.enqueue(require_constants.GLOBAL_TSR + ".e()");
scriptBuffer.flush();
signalSerializationComplete();
};
(0, seroval.crossSerializeStream)(dehydratedRouter, {
refs: /* @__PURE__ */ new Map(),
plugins,
onSerialize: (data, initial) => {
let serialized = initial ? TSR_PREFIX + data : data;
if (trackPlugins.didRun) serialized = P_PREFIX + serialized + P_SUFFIX;
scriptBuffer.enqueue(serialized);
},
onError: (err) => {
console.error("Serialization error:", err);
if (err && err.stack) console.error(err.stack);
finishScriptSerialization();
},
scopeId: SCOPE_ID,
onDone: () => {
finishScriptSerialization();
}
});
},
isDehydrated() {
return _dehydrated;
},
isSerializationFinished() {
return _serializationFinished;
},
reserveStreamFastPath() {
if (!cleanupStarted && _serializationFinished && !streamFastPathReserved && renderFinishedListeners.length === 0 && !injectedHtmlBuffer && !scriptBuffer.hasPending()) {
streamFastPathReserved = true;
return true;
}
return false;
},
onInjectedHtml: (listener) => {
if (cleanupStarted) return () => {};
injectedHtmlListeners.push(listener);
return () => removeListener(injectedHtmlListeners, listener);
},
onRenderFinished: (listener) => {
if (cleanupStarted || streamFastPathReserved) return;
renderFinishedListeners.push(listener);
},
onSerializationFinished: (listener) => {
if (cleanupStarted) return () => {};
if (_serializationFinished && !cleanupStarted) {
try {
listener();
} catch (err) {
console.error("Serialization listener error:", err);
}
return () => {};
}
serializationFinishedListeners.push(listener);
return () => removeListener(serializationFinishedListeners, listener);
},
onCleanup: (listener) => {
if (cleanupStarted) return;
cleanupListeners.push(listener);
},
setRenderFinished: () => {
if (cleanupStarted) return;
scriptBuffer.liftBarrier();
const listeners = renderFinishedListeners.slice();
renderFinishedListeners.length = 0;
for (const l of listeners) try {
l();
} catch (err) {
console.error("Error in render finished listener:", err);
}
if (_serializationFinished) scriptBuffer.flush();
},
takeBufferedScripts() {
const scripts = scriptBuffer.takeAll();
if (!scripts) return void 0;
return {
tag: "script",
attrs: {
nonce: router.options.ssr?.nonce,
className: "$tsr",
id: require_constants.TSR_SCRIPT_BARRIER_ID
},
children: scripts
};
},
liftScriptBarrier() {
scriptBuffer.liftBarrier();
},
takeBufferedHtml() {
if (!injectedHtmlBuffer) return;
const buffered = injectedHtmlBuffer;
injectedHtmlBuffer = "";
return buffered;
},
cleanup() {
if (cleanupStarted) return;
cleanupStarted = true;
const listeners = cleanupListeners.slice();
cleanupListeners.length = 0;
for (const l of listeners) try {
l();
} catch (err) {
console.error("Error in SSR cleanup listener:", err);
}
renderFinishedListeners.length = 0;
injectedHtmlListeners.length = 0;
serializationFinishedListeners.length = 0;
injectedHtmlBuffer = "";
scriptBuffer.cleanup();
router.serverSsr = void 0;
}
};
router.serverSsr = serverSsr;
for (const listener of router.serverSsrLifecycle?.onServerSsrAttach ?? []) try {
listener(serverSsr);
} catch (err) {
console.error("SSR attach listener error:", err);
}
}
/**
* Get the origin for the request.
*
* SECURITY: We intentionally do NOT trust the Origin header for determining
* the router's origin. The Origin header can be spoofed by attackers, which
* could lead to SSRF-like vulnerabilities where redirects are constructed
* using a malicious origin (CVE-2024-34351).
*
* Instead, we derive the origin from request.url, which is typically set by
* the server infrastructure (not client-controlled headers).
*
* For applications behind proxies that need to trust forwarded headers,
* use the router's `origin` option to explicitly configure a trusted origin.
*/
function getOrigin(request) {
try {
return new URL(request.url).origin;
} catch {}
return "http://localhost";
}
function getNormalizedURL(url, base) {
if (typeof url === "string") url = url.replace("\\", "%5C");
const rawUrl = new URL(url, base);
const { path: decodedPathname, handledProtocolRelativeURL } = require_utils.decodePath(rawUrl.pathname);
const searchParams = new URLSearchParams(rawUrl.search);
const normalizedHref = decodedPathname + (searchParams.size > 0 ? "?" : "") + searchParams.toString() + rawUrl.hash;
return {
url: new URL(normalizedHref, rawUrl.origin),
handledProtocolRelativeURL
};
}
//#endregion
exports.attachRouterServerSsrUtils = attachRouterServerSsrUtils;
exports.getNormalizedURL = getNormalizedURL;
exports.getOrigin = getOrigin;
//# sourceMappingURL=ssr-server.cjs.map