UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

278 lines (277 loc) 10.1 kB
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