@tanstack/router-core
Version:
Modern and scalable routing for React applications
278 lines (277 loc) • 10.1 kB
JavaScript
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
const require_utils = require("../utils.cjs");
const require_lru_cache = require("../lru-cache.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 tiny_invariant = require("tiny-invariant");
tiny_invariant = require_runtime.__toESM(tiny_invariant);
let seroval = require("seroval");
//#region src/ssr/ssr-server.ts
var SCOPE_ID = "tsr";
var TSR_PREFIX = require_constants.GLOBAL_TSR + ".router=";
var P_PREFIX = require_constants.GLOBAL_TSR + ".p(()=>";
var 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;
}
var INITIAL_SCRIPTS = [(0, seroval.getCrossReferenceHeader)(SCOPE_ID), require_tsrScript.default];
var ScriptBuffer = class {
constructor(router) {
this._scriptBarrierLifted = false;
this._cleanedUp = false;
this._pendingMicrotask = false;
this.router = router;
this._queue = INITIAL_SCRIPTS.slice();
}
enqueue(script) {
if (this._cleanedUp) return;
this._queue.push(script);
if (this._scriptBarrierLifted && !this._pendingMicrotask) {
this._pendingMicrotask = true;
queueMicrotask(() => {
this._pendingMicrotask = false;
this.injectBufferedScripts();
});
}
}
liftBarrier() {
if (this._scriptBarrierLifted || this._cleanedUp) return;
this._scriptBarrierLifted = true;
if (this._queue.length > 0 && !this._pendingMicrotask) {
this._pendingMicrotask = true;
queueMicrotask(() => {
this._pendingMicrotask = false;
this.injectBufferedScripts();
});
}
}
/**
* Flushes any pending scripts synchronously.
* Call this before emitting onSerializationFinished 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._pendingMicrotask = false;
const scriptsToInject = this.takeAll();
if (scriptsToInject && this.router?.serverSsr) this.router.serverSsr.injectScript(scriptsToInject);
}
takeAll() {
const bufferedScripts = this._queue;
this._queue = [];
if (bufferedScripts.length === 0) return;
if (bufferedScripts.length === 1) return bufferedScripts[0] + ";document.currentScript.remove()";
return bufferedScripts.join(";") + ";document.currentScript.remove()";
}
injectBufferedScripts() {
if (this._cleanedUp) return;
if (this._queue.length === 0) return;
const scriptsToInject = this.takeAll();
if (scriptsToInject && this.router?.serverSsr) this.router.serverSsr.injectScript(scriptsToInject);
}
cleanup() {
this._cleanedUp = true;
this._queue = [];
this.router = void 0;
}
};
var isProd = process.env.NODE_ENV === "production";
var MANIFEST_CACHE_SIZE = 100;
var 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 attachRouterServerSsrUtils({ router, manifest }) {
router.ssr = { manifest };
let _dehydrated = false;
let _serializationFinished = false;
const renderFinishedListeners = [];
const serializationFinishedListeners = [];
const scriptBuffer = new ScriptBuffer(router);
let injectedHtmlBuffer = "";
router.serverSsr = {
injectHtml: (html) => {
if (!html) return;
injectedHtmlBuffer += html;
router.emit({ type: "onInjectedHtml" });
},
injectScript: (script) => {
if (!script) return;
const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ""}>${script}<\/script>`;
router.serverSsr.injectHtml(html);
},
dehydrate: async () => {
(0, tiny_invariant.default)(!_dehydrated, "router is already dehydrated!");
let matchesToDehydrate = router.state.matches;
if (router.isShell()) matchesToDehydrate = matchesToDehydrate.slice(0, 1);
const matches = matchesToDehydrate.map(dehydrateMatch);
let manifestToDehydrate = void 0;
if (manifest) {
const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId);
const manifestCacheKey = currentRouteIdsList.join("\0");
let filteredRoutes;
if (isProd) filteredRoutes = getManifestCache(manifest).get(manifestCacheKey);
if (!filteredRoutes) {
const currentRouteIds = new Set(currentRouteIdsList);
const nextFilteredRoutes = {};
for (const routeId in manifest.routes) {
const routeManifest = manifest.routes[routeId];
if (currentRouteIds.has(routeId)) nextFilteredRoutes[routeId] = routeManifest;
else if (routeManifest.assets && routeManifest.assets.length > 0) nextFilteredRoutes[routeId] = { assets: routeManifest.assets };
}
if (isProd) getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes);
filteredRoutes = nextFilteredRoutes;
}
manifestToDehydrate = { routes: filteredRoutes };
}
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) => require_transformer.makeSsrSerovalPlugin(t, trackPlugins)).concat(require_seroval_plugins.defaultSerovalPlugins) : require_seroval_plugins.defaultSerovalPlugins;
const signalSerializationComplete = () => {
_serializationFinished = true;
try {
serializationFinishedListeners.forEach((l) => l());
router.emit({ type: "onSerializationFinished" });
} catch (err) {
console.error("Serialization listener error:", err);
} finally {
serializationFinishedListeners.length = 0;
renderFinishedListeners.length = 0;
}
};
(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);
},
scopeId: SCOPE_ID,
onDone: () => {
scriptBuffer.enqueue(require_constants.GLOBAL_TSR + ".e()");
scriptBuffer.flush();
signalSerializationComplete();
},
onError: (err) => {
console.error("Serialization error:", err);
signalSerializationComplete();
}
});
},
isDehydrated() {
return _dehydrated;
},
isSerializationFinished() {
return _serializationFinished;
},
onRenderFinished: (listener) => renderFinishedListeners.push(listener),
onSerializationFinished: (listener) => serializationFinishedListeners.push(listener),
setRenderFinished: () => {
try {
renderFinishedListeners.forEach((l) => l());
} catch (err) {
console.error("Error in render finished listener:", err);
} finally {
renderFinishedListeners.length = 0;
}
scriptBuffer.liftBarrier();
},
takeBufferedScripts() {
const scripts = scriptBuffer.takeAll();
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 (!router.serverSsr) return;
renderFinishedListeners.length = 0;
serializationFinishedListeners.length = 0;
injectedHtmlBuffer = "";
scriptBuffer.cleanup();
router.serverSsr = void 0;
}
};
}
/**
* 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