@tanstack/router-core 1.157.4 → 1.157.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/load-matches.cjs +4 -4
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/router.cjs +29 -13
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +0 -9
- package/dist/cjs/ssr/ssr-client.cjs +2 -2
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +48 -29
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +138 -42
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/utils/batch.cjs +16 -0
- package/dist/cjs/utils/batch.cjs.map +1 -0
- package/dist/cjs/utils/batch.d.cts +1 -0
- package/dist/cjs/utils.cjs +4 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/load-matches.js +1 -1
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.d.ts +0 -9
- package/dist/esm/router.js +28 -12
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-client.js +1 -1
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +48 -29
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +138 -42
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/dist/esm/utils/batch.d.ts +1 -0
- package/dist/esm/utils/batch.js +16 -0
- package/dist/esm/utils/batch.js.map +1 -0
- package/dist/esm/utils.js +4 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/load-matches.ts +1 -1
- package/src/router.ts +39 -14
- package/src/ssr/ssr-client.ts +1 -1
- package/src/ssr/ssr-server.ts +69 -32
- package/src/ssr/transformStreamWithRouter.ts +187 -108
- package/src/utils/batch.ts +18 -0
- package/src/utils.ts +4 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import invariant from "tiny-invariant";
|
|
2
|
-
import { batch } from "
|
|
2
|
+
import { batch } from "../utils/batch.js";
|
|
3
3
|
import { isNotFound } from "../not-found.js";
|
|
4
4
|
import { createControlledPromise } from "../utils.js";
|
|
5
5
|
function hydrateMatch(match, deyhydratedMatch) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-client.js","sources":["../../../src/ssr/ssr-client.ts"],"sourcesContent":["import invariant from 'tiny-invariant'\nimport { batch } from '@tanstack/store'\nimport { isNotFound } from '../not-found'\nimport { createControlledPromise } from '../utils'\nimport type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'\nimport type { DehydratedMatch, TsrSsrGlobal } from './types'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { AnyRouter } from '../router'\nimport type { RouteContextOptions } from '../route'\nimport type { AnySerializationAdapter } from './serializer/transformer'\n\ndeclare global {\n interface Window {\n [GLOBAL_TSR]?: TsrSsrGlobal\n [GLOBAL_SEROVAL]?: any\n }\n}\n\nfunction hydrateMatch(\n match: AnyRouteMatch,\n deyhydratedMatch: DehydratedMatch,\n): void {\n match.id = deyhydratedMatch.i\n match.__beforeLoadContext = deyhydratedMatch.b\n match.loaderData = deyhydratedMatch.l\n match.status = deyhydratedMatch.s\n match.ssr = deyhydratedMatch.ssr\n match.updatedAt = deyhydratedMatch.u\n match.error = deyhydratedMatch.e\n}\n\nexport async function hydrate(router: AnyRouter): Promise<any> {\n invariant(\n window.$_TSR,\n 'Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',\n )\n\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n\n if (serializationAdapters?.length) {\n const fromSerializableMap = new Map()\n serializationAdapters.forEach((adapter) => {\n fromSerializableMap.set(adapter.key, adapter.fromSerializable)\n })\n window.$_TSR.t = fromSerializableMap\n window.$_TSR.buffer.forEach((script) => script())\n }\n window.$_TSR.initialized = true\n\n invariant(\n window.$_TSR.router,\n 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',\n )\n\n const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router\n\n router.ssr = {\n manifest,\n }\n const meta = document.querySelector('meta[property=\"csp-nonce\"]') as\n | HTMLMetaElement\n | undefined\n const nonce = meta?.content\n router.options.ssr = {\n nonce,\n }\n\n // Hydrate the router state\n const matches = router.matchRoutes(router.state.location)\n\n // kick off loading the route chunks\n const routeChunkPromise = Promise.all(\n matches.map((match) => {\n const route = router.looseRoutesById[match.routeId]!\n return router.loadRouteChunk(route)\n }),\n )\n\n function setMatchForcePending(match: AnyRouteMatch) {\n // usually the minPendingPromise is created in the Match component if a pending match is rendered\n // however, this might be too late if the match synchronously resolves\n const route = router.looseRoutesById[match.routeId]!\n const pendingMinMs =\n route.options.pendingMinMs ?? router.options.defaultPendingMinMs\n if (pendingMinMs) {\n const minPendingPromise = createControlledPromise<void>()\n match._nonReactive.minPendingPromise = minPendingPromise\n match._forcePending = true\n\n setTimeout(() => {\n minPendingPromise.resolve()\n // We've handled the minPendingPromise, so we can delete it\n router.updateMatch(match.id, (prev) => {\n prev._nonReactive.minPendingPromise = undefined\n return {\n ...prev,\n _forcePending: undefined,\n }\n })\n }, pendingMinMs)\n }\n }\n\n function setRouteSsr(match: AnyRouteMatch) {\n const route = router.looseRoutesById[match.routeId]\n if (route) {\n route.options.ssr = match.ssr\n }\n }\n // Right after hydration and before the first render, we need to rehydrate each match\n // First step is to reyhdrate loaderData and __beforeLoadContext\n let firstNonSsrMatchIndex: number | undefined = undefined\n matches.forEach((match) => {\n const dehydratedMatch = window.$_TSR!.router!.matches.find(\n (d) => d.i === match.id,\n )\n if (!dehydratedMatch) {\n match._nonReactive.dehydrated = false\n match.ssr = false\n setRouteSsr(match)\n return\n }\n\n hydrateMatch(match, dehydratedMatch)\n setRouteSsr(match)\n\n match._nonReactive.dehydrated = match.ssr !== false\n\n if (match.ssr === 'data-only' || match.ssr === false) {\n if (firstNonSsrMatchIndex === undefined) {\n firstNonSsrMatchIndex = match.index\n setMatchForcePending(match)\n }\n }\n })\n\n router.__store.setState((s) => {\n return {\n ...s,\n matches,\n }\n })\n\n // Allow the user to handle custom hydration data\n await router.options.hydrate?.(dehydratedData)\n\n // now that all necessary data is hydrated:\n // 1) fully reconstruct the route context\n // 2) execute `head()` and `scripts()` for each match\n await Promise.all(\n router.state.matches.map(async (match) => {\n try {\n const route = router.looseRoutesById[match.routeId]!\n\n const parentMatch = router.state.matches[match.index - 1]\n const parentContext = parentMatch?.context ?? router.options.context\n\n // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed\n // so run it again and merge route context\n if (route.options.context) {\n const contextFnContext: RouteContextOptions<any, any, any, any> = {\n deps: match.loaderDeps,\n params: match.params,\n context: parentContext ?? {},\n location: router.state.location,\n navigate: (opts: any) =>\n router.navigate({\n ...opts,\n _fromLocation: router.state.location,\n }),\n buildLocation: router.buildLocation,\n cause: match.cause,\n abortController: match.abortController,\n preload: false,\n matches,\n }\n match.__routeContext =\n route.options.context(contextFnContext) ?? undefined\n }\n\n match.context = {\n ...parentContext,\n ...match.__routeContext,\n ...match.__beforeLoadContext,\n }\n\n const assetContext = {\n ssr: router.options.ssr,\n matches: router.state.matches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const headFnContent = await route.options.head?.(assetContext)\n\n const scripts = await route.options.scripts?.(assetContext)\n\n match.meta = headFnContent?.meta\n match.links = headFnContent?.links\n match.headScripts = headFnContent?.scripts\n match.styles = headFnContent?.styles\n match.scripts = scripts\n } catch (err) {\n if (isNotFound(err)) {\n match.error = { isNotFound: true }\n console.error(\n `NotFound error during hydration for routeId: ${match.routeId}`,\n err,\n )\n } else {\n match.error = err as any\n console.error(\n `Error during hydration for route ${match.routeId}:`,\n err,\n )\n throw err\n }\n }\n }),\n )\n\n const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId\n const hasSsrFalseMatches = matches.some((m) => m.ssr === false)\n // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()\n if (!hasSsrFalseMatches && !isSpaMode) {\n matches.forEach((match) => {\n // remove the dehydrated flag since we won't run router.load() which would remove it\n match._nonReactive.dehydrated = undefined\n })\n return routeChunkPromise\n }\n\n // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts\n const loadPromise = Promise.resolve()\n .then(() => router.load())\n .catch((err) => {\n console.error('Error during router hydration:', err)\n })\n\n // in SPA mode we need to keep the first match below the root route pending until router.load() is finished\n // this will prevent that other pending components are rendered but hydration is not blocked\n if (isSpaMode) {\n const match = matches[1]\n invariant(\n match,\n 'Expected to find a match below the root match in SPA mode.',\n )\n setMatchForcePending(match)\n\n match._displayPending = true\n match._nonReactive.displayPendingPromise = loadPromise\n\n loadPromise.then(() => {\n batch(() => {\n // ensure router is not in status 'pending' anymore\n // this usually happens in Transitioner but if loading synchronously resolves,\n // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false\n if (router.__store.state.status === 'pending') {\n router.__store.setState((s) => ({\n ...s,\n status: 'idle',\n resolvedLocation: s.location,\n }))\n }\n // hide the pending component once the load is finished\n router.updateMatch(match.id, (prev) => {\n return {\n ...prev,\n _displayPending: undefined,\n displayPendingPromise: undefined,\n }\n })\n })\n })\n }\n return routeChunkPromise\n}\n"],"names":[],"mappings":";;;;AAkBA,SAAS,aACP,OACA,kBACM;AACN,QAAM,KAAK,iBAAiB;AAC5B,QAAM,sBAAsB,iBAAiB;AAC7C,QAAM,aAAa,iBAAiB;AACpC,QAAM,SAAS,iBAAiB;AAChC,QAAM,MAAM,iBAAiB;AAC7B,QAAM,YAAY,iBAAiB;AACnC,QAAM,QAAQ,iBAAiB;AACjC;AAEA,eAAsB,QAAQ,QAAiC;AAC7D;AAAA,IACE,OAAO;AAAA,IACP;AAAA,EAAA;AAGF,QAAM,wBAAwB,OAAO,QAAQ;AAI7C,MAAI,uBAAuB,QAAQ;AACjC,UAAM,0CAA0B,IAAA;AAChC,0BAAsB,QAAQ,CAAC,YAAY;AACzC,0BAAoB,IAAI,QAAQ,KAAK,QAAQ,gBAAgB;AAAA,IAC/D,CAAC;AACD,WAAO,MAAM,IAAI;AACjB,WAAO,MAAM,OAAO,QAAQ,CAAC,WAAW,QAAQ;AAAA,EAClD;AACA,SAAO,MAAM,cAAc;AAE3B;AAAA,IACE,OAAO,MAAM;AAAA,IACb;AAAA,EAAA;AAGF,QAAM,EAAE,UAAU,gBAAgB,YAAA,IAAgB,OAAO,MAAM;AAE/D,SAAO,MAAM;AAAA,IACX;AAAA,EAAA;AAEF,QAAM,OAAO,SAAS,cAAc,4BAA4B;AAGhE,QAAM,QAAQ,MAAM;AACpB,SAAO,QAAQ,MAAM;AAAA,IACnB;AAAA,EAAA;AAIF,QAAM,UAAU,OAAO,YAAY,OAAO,MAAM,QAAQ;AAGxD,QAAM,oBAAoB,QAAQ;AAAA,IAChC,QAAQ,IAAI,CAAC,UAAU;AACrB,YAAM,QAAQ,OAAO,gBAAgB,MAAM,OAAO;AAClD,aAAO,OAAO,eAAe,KAAK;AAAA,IACpC,CAAC;AAAA,EAAA;AAGH,WAAS,qBAAqB,OAAsB;AAGlD,UAAM,QAAQ,OAAO,gBAAgB,MAAM,OAAO;AAClD,UAAM,eACJ,MAAM,QAAQ,gBAAgB,OAAO,QAAQ;AAC/C,QAAI,cAAc;AAChB,YAAM,oBAAoB,wBAAA;AAC1B,YAAM,aAAa,oBAAoB;AACvC,YAAM,gBAAgB;AAEtB,iBAAW,MAAM;AACf,0BAAkB,QAAA;AAElB,eAAO,YAAY,MAAM,IAAI,CAAC,SAAS;AACrC,eAAK,aAAa,oBAAoB;AACtC,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,eAAe;AAAA,UAAA;AAAA,QAEnB,CAAC;AAAA,MACH,GAAG,YAAY;AAAA,IACjB;AAAA,EACF;AAEA,WAAS,YAAY,OAAsB;AACzC,UAAM,QAAQ,OAAO,gBAAgB,MAAM,OAAO;AAClD,QAAI,OAAO;AACT,YAAM,QAAQ,MAAM,MAAM;AAAA,IAC5B;AAAA,EACF;AAGA,MAAI,wBAA4C;AAChD,UAAQ,QAAQ,CAAC,UAAU;AACzB,UAAM,kBAAkB,OAAO,MAAO,OAAQ,QAAQ;AAAA,MACpD,CAAC,MAAM,EAAE,MAAM,MAAM;AAAA,IAAA;AAEvB,QAAI,CAAC,iBAAiB;AACpB,YAAM,aAAa,aAAa;AAChC,YAAM,MAAM;AACZ,kBAAY,KAAK;AACjB;AAAA,IACF;AAEA,iBAAa,OAAO,eAAe;AACnC,gBAAY,KAAK;AAEjB,UAAM,aAAa,aAAa,MAAM,QAAQ;AAE9C,QAAI,MAAM,QAAQ,eAAe,MAAM,QAAQ,OAAO;AACpD,UAAI,0BAA0B,QAAW;AACvC,gCAAwB,MAAM;AAC9B,6BAAqB,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,QAAQ,SAAS,CAAC,MAAM;AAC7B,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,IAAA;AAAA,EAEJ,CAAC;AAGD,QAAM,OAAO,QAAQ,UAAU,cAAc;AAK7C,QAAM,QAAQ;AAAA,IACZ,OAAO,MAAM,QAAQ,IAAI,OAAO,UAAU;AACxC,UAAI;AACF,cAAM,QAAQ,OAAO,gBAAgB,MAAM,OAAO;AAElD,cAAM,cAAc,OAAO,MAAM,QAAQ,MAAM,QAAQ,CAAC;AACxD,cAAM,gBAAgB,aAAa,WAAW,OAAO,QAAQ;AAI7D,YAAI,MAAM,QAAQ,SAAS;AACzB,gBAAM,mBAA4D;AAAA,YAChE,MAAM,MAAM;AAAA,YACZ,QAAQ,MAAM;AAAA,YACd,SAAS,iBAAiB,CAAA;AAAA,YAC1B,UAAU,OAAO,MAAM;AAAA,YACvB,UAAU,CAAC,SACT,OAAO,SAAS;AAAA,cACd,GAAG;AAAA,cACH,eAAe,OAAO,MAAM;AAAA,YAAA,CAC7B;AAAA,YACH,eAAe,OAAO;AAAA,YACtB,OAAO,MAAM;AAAA,YACb,iBAAiB,MAAM;AAAA,YACvB,SAAS;AAAA,YACT;AAAA,UAAA;AAEF,gBAAM,iBACJ,MAAM,QAAQ,QAAQ,gBAAgB,KAAK;AAAA,QAC/C;AAEA,cAAM,UAAU;AAAA,UACd,GAAG;AAAA,UACH,GAAG,MAAM;AAAA,UACT,GAAG,MAAM;AAAA,QAAA;AAGX,cAAM,eAAe;AAAA,UACnB,KAAK,OAAO,QAAQ;AAAA,UACpB,SAAS,OAAO,MAAM;AAAA,UACtB;AAAA,UACA,QAAQ,MAAM;AAAA,UACd,YAAY,MAAM;AAAA,QAAA;AAEpB,cAAM,gBAAgB,MAAM,MAAM,QAAQ,OAAO,YAAY;AAE7D,cAAM,UAAU,MAAM,MAAM,QAAQ,UAAU,YAAY;AAE1D,cAAM,OAAO,eAAe;AAC5B,cAAM,QAAQ,eAAe;AAC7B,cAAM,cAAc,eAAe;AACnC,cAAM,SAAS,eAAe;AAC9B,cAAM,UAAU;AAAA,MAClB,SAAS,KAAK;AACZ,YAAI,WAAW,GAAG,GAAG;AACnB,gBAAM,QAAQ,EAAE,YAAY,KAAA;AAC5B,kBAAQ;AAAA,YACN,gDAAgD,MAAM,OAAO;AAAA,YAC7D;AAAA,UAAA;AAAA,QAEJ,OAAO;AACL,gBAAM,QAAQ;AACd,kBAAQ;AAAA,YACN,oCAAoC,MAAM,OAAO;AAAA,YACjD;AAAA,UAAA;AAEF,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EAAA;AAGH,QAAM,YAAY,QAAQ,QAAQ,SAAS,CAAC,EAAG,OAAO;AACtD,QAAM,qBAAqB,QAAQ,KAAK,CAAC,MAAM,EAAE,QAAQ,KAAK;AAE9D,MAAI,CAAC,sBAAsB,CAAC,WAAW;AACrC,YAAQ,QAAQ,CAAC,UAAU;AAEzB,YAAM,aAAa,aAAa;AAAA,IAClC,CAAC;AACD,WAAO;AAAA,EACT;AAGA,QAAM,cAAc,QAAQ,QAAA,EACzB,KAAK,MAAM,OAAO,KAAA,CAAM,EACxB,MAAM,CAAC,QAAQ;AACd,YAAQ,MAAM,kCAAkC,GAAG;AAAA,EACrD,CAAC;AAIH,MAAI,WAAW;AACb,UAAM,QAAQ,QAAQ,CAAC;AACvB;AAAA,MACE;AAAA,MACA;AAAA,IAAA;AAEF,yBAAqB,KAAK;AAE1B,UAAM,kBAAkB;AACxB,UAAM,aAAa,wBAAwB;AAE3C,gBAAY,KAAK,MAAM;AACrB,YAAM,MAAM;AAIV,YAAI,OAAO,QAAQ,MAAM,WAAW,WAAW;AAC7C,iBAAO,QAAQ,SAAS,CAAC,OAAO;AAAA,YAC9B,GAAG;AAAA,YACH,QAAQ;AAAA,YACR,kBAAkB,EAAE;AAAA,UAAA,EACpB;AAAA,QACJ;AAEA,eAAO,YAAY,MAAM,IAAI,CAAC,SAAS;AACrC,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,iBAAiB;AAAA,YACjB,uBAAuB;AAAA,UAAA;AAAA,QAE3B,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACA,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"ssr-client.js","sources":["../../../src/ssr/ssr-client.ts"],"sourcesContent":["import invariant from 'tiny-invariant'\nimport { batch } from '../utils/batch'\nimport { isNotFound } from '../not-found'\nimport { createControlledPromise } from '../utils'\nimport type { GLOBAL_SEROVAL, GLOBAL_TSR } from './constants'\nimport type { DehydratedMatch, TsrSsrGlobal } from './types'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { AnyRouter } from '../router'\nimport type { RouteContextOptions } from '../route'\nimport type { AnySerializationAdapter } from './serializer/transformer'\n\ndeclare global {\n interface Window {\n [GLOBAL_TSR]?: TsrSsrGlobal\n [GLOBAL_SEROVAL]?: any\n }\n}\n\nfunction hydrateMatch(\n match: AnyRouteMatch,\n deyhydratedMatch: DehydratedMatch,\n): void {\n match.id = deyhydratedMatch.i\n match.__beforeLoadContext = deyhydratedMatch.b\n match.loaderData = deyhydratedMatch.l\n match.status = deyhydratedMatch.s\n match.ssr = deyhydratedMatch.ssr\n match.updatedAt = deyhydratedMatch.u\n match.error = deyhydratedMatch.e\n}\n\nexport async function hydrate(router: AnyRouter): Promise<any> {\n invariant(\n window.$_TSR,\n 'Expected to find bootstrap data on window.$_TSR, but we did not. Please file an issue!',\n )\n\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n\n if (serializationAdapters?.length) {\n const fromSerializableMap = new Map()\n serializationAdapters.forEach((adapter) => {\n fromSerializableMap.set(adapter.key, adapter.fromSerializable)\n })\n window.$_TSR.t = fromSerializableMap\n window.$_TSR.buffer.forEach((script) => script())\n }\n window.$_TSR.initialized = true\n\n invariant(\n window.$_TSR.router,\n 'Expected to find a dehydrated data on window.$_TSR.router, but we did not. Please file an issue!',\n )\n\n const { manifest, dehydratedData, lastMatchId } = window.$_TSR.router\n\n router.ssr = {\n manifest,\n }\n const meta = document.querySelector('meta[property=\"csp-nonce\"]') as\n | HTMLMetaElement\n | undefined\n const nonce = meta?.content\n router.options.ssr = {\n nonce,\n }\n\n // Hydrate the router state\n const matches = router.matchRoutes(router.state.location)\n\n // kick off loading the route chunks\n const routeChunkPromise = Promise.all(\n matches.map((match) => {\n const route = router.looseRoutesById[match.routeId]!\n return router.loadRouteChunk(route)\n }),\n )\n\n function setMatchForcePending(match: AnyRouteMatch) {\n // usually the minPendingPromise is created in the Match component if a pending match is rendered\n // however, this might be too late if the match synchronously resolves\n const route = router.looseRoutesById[match.routeId]!\n const pendingMinMs =\n route.options.pendingMinMs ?? router.options.defaultPendingMinMs\n if (pendingMinMs) {\n const minPendingPromise = createControlledPromise<void>()\n match._nonReactive.minPendingPromise = minPendingPromise\n match._forcePending = true\n\n setTimeout(() => {\n minPendingPromise.resolve()\n // We've handled the minPendingPromise, so we can delete it\n router.updateMatch(match.id, (prev) => {\n prev._nonReactive.minPendingPromise = undefined\n return {\n ...prev,\n _forcePending: undefined,\n }\n })\n }, pendingMinMs)\n }\n }\n\n function setRouteSsr(match: AnyRouteMatch) {\n const route = router.looseRoutesById[match.routeId]\n if (route) {\n route.options.ssr = match.ssr\n }\n }\n // Right after hydration and before the first render, we need to rehydrate each match\n // First step is to reyhdrate loaderData and __beforeLoadContext\n let firstNonSsrMatchIndex: number | undefined = undefined\n matches.forEach((match) => {\n const dehydratedMatch = window.$_TSR!.router!.matches.find(\n (d) => d.i === match.id,\n )\n if (!dehydratedMatch) {\n match._nonReactive.dehydrated = false\n match.ssr = false\n setRouteSsr(match)\n return\n }\n\n hydrateMatch(match, dehydratedMatch)\n setRouteSsr(match)\n\n match._nonReactive.dehydrated = match.ssr !== false\n\n if (match.ssr === 'data-only' || match.ssr === false) {\n if (firstNonSsrMatchIndex === undefined) {\n firstNonSsrMatchIndex = match.index\n setMatchForcePending(match)\n }\n }\n })\n\n router.__store.setState((s) => {\n return {\n ...s,\n matches,\n }\n })\n\n // Allow the user to handle custom hydration data\n await router.options.hydrate?.(dehydratedData)\n\n // now that all necessary data is hydrated:\n // 1) fully reconstruct the route context\n // 2) execute `head()` and `scripts()` for each match\n await Promise.all(\n router.state.matches.map(async (match) => {\n try {\n const route = router.looseRoutesById[match.routeId]!\n\n const parentMatch = router.state.matches[match.index - 1]\n const parentContext = parentMatch?.context ?? router.options.context\n\n // `context()` was already executed by `matchRoutes`, however route context was not yet fully reconstructed\n // so run it again and merge route context\n if (route.options.context) {\n const contextFnContext: RouteContextOptions<any, any, any, any> = {\n deps: match.loaderDeps,\n params: match.params,\n context: parentContext ?? {},\n location: router.state.location,\n navigate: (opts: any) =>\n router.navigate({\n ...opts,\n _fromLocation: router.state.location,\n }),\n buildLocation: router.buildLocation,\n cause: match.cause,\n abortController: match.abortController,\n preload: false,\n matches,\n }\n match.__routeContext =\n route.options.context(contextFnContext) ?? undefined\n }\n\n match.context = {\n ...parentContext,\n ...match.__routeContext,\n ...match.__beforeLoadContext,\n }\n\n const assetContext = {\n ssr: router.options.ssr,\n matches: router.state.matches,\n match,\n params: match.params,\n loaderData: match.loaderData,\n }\n const headFnContent = await route.options.head?.(assetContext)\n\n const scripts = await route.options.scripts?.(assetContext)\n\n match.meta = headFnContent?.meta\n match.links = headFnContent?.links\n match.headScripts = headFnContent?.scripts\n match.styles = headFnContent?.styles\n match.scripts = scripts\n } catch (err) {\n if (isNotFound(err)) {\n match.error = { isNotFound: true }\n console.error(\n `NotFound error during hydration for routeId: ${match.routeId}`,\n err,\n )\n } else {\n match.error = err as any\n console.error(\n `Error during hydration for route ${match.routeId}:`,\n err,\n )\n throw err\n }\n }\n }),\n )\n\n const isSpaMode = matches[matches.length - 1]!.id !== lastMatchId\n const hasSsrFalseMatches = matches.some((m) => m.ssr === false)\n // all matches have data from the server and we are not in SPA mode so we don't need to kick of router.load()\n if (!hasSsrFalseMatches && !isSpaMode) {\n matches.forEach((match) => {\n // remove the dehydrated flag since we won't run router.load() which would remove it\n match._nonReactive.dehydrated = undefined\n })\n return routeChunkPromise\n }\n\n // schedule router.load() to run after the next tick so we can store the promise in the match before loading starts\n const loadPromise = Promise.resolve()\n .then(() => router.load())\n .catch((err) => {\n console.error('Error during router hydration:', err)\n })\n\n // in SPA mode we need to keep the first match below the root route pending until router.load() is finished\n // this will prevent that other pending components are rendered but hydration is not blocked\n if (isSpaMode) {\n const match = matches[1]\n invariant(\n match,\n 'Expected to find a match below the root match in SPA mode.',\n )\n setMatchForcePending(match)\n\n match._displayPending = true\n match._nonReactive.displayPendingPromise = loadPromise\n\n loadPromise.then(() => {\n batch(() => {\n // ensure router is not in status 'pending' anymore\n // this usually happens in Transitioner but if loading synchronously resolves,\n // Transitioner won't be rendered while loading so it cannot track the change from loading:true to loading:false\n if (router.__store.state.status === 'pending') {\n router.__store.setState((s) => ({\n ...s,\n status: 'idle',\n resolvedLocation: s.location,\n }))\n }\n // hide the pending component once the load is finished\n router.updateMatch(match.id, (prev) => {\n return {\n ...prev,\n _displayPending: undefined,\n displayPendingPromise: undefined,\n }\n })\n })\n })\n }\n return routeChunkPromise\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { crossSerializeStream, getCrossReferenceHeader } from "seroval";
|
|
2
2
|
import invariant from "tiny-invariant";
|
|
3
3
|
import { decodePath } from "../utils.js";
|
|
4
|
+
import { createLRUCache } from "../lru-cache.js";
|
|
4
5
|
import minifiedTsrBootStrapScript from "./tsrScript.js";
|
|
5
6
|
import { TSR_SCRIPT_BARRIER_ID, GLOBAL_TSR } from "./constants.js";
|
|
6
7
|
import { defaultSerovalPlugins } from "./serializer/seroval-plugins.js";
|
|
7
8
|
import { makeSsrSerovalPlugin } from "./serializer/transformer.js";
|
|
8
9
|
const SCOPE_ID = "tsr";
|
|
10
|
+
const TSR_PREFIX = GLOBAL_TSR + ".router=";
|
|
11
|
+
const P_PREFIX = GLOBAL_TSR + ".p(()=>";
|
|
12
|
+
const P_SUFFIX = ")";
|
|
9
13
|
function dehydrateMatch(match) {
|
|
10
14
|
const dehydratedMatch = {
|
|
11
15
|
i: match.id,
|
|
@@ -81,6 +85,9 @@ class ScriptBuffer {
|
|
|
81
85
|
if (bufferedScripts.length === 0) {
|
|
82
86
|
return void 0;
|
|
83
87
|
}
|
|
88
|
+
if (bufferedScripts.length === 1) {
|
|
89
|
+
return bufferedScripts[0] + ";document.currentScript.remove()";
|
|
90
|
+
}
|
|
84
91
|
return bufferedScripts.join(";") + ";document.currentScript.remove()";
|
|
85
92
|
}
|
|
86
93
|
injectBufferedScripts() {
|
|
@@ -97,6 +104,16 @@ class ScriptBuffer {
|
|
|
97
104
|
this.router = void 0;
|
|
98
105
|
}
|
|
99
106
|
}
|
|
107
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
108
|
+
const MANIFEST_CACHE_SIZE = 100;
|
|
109
|
+
const manifestCaches = /* @__PURE__ */ new WeakMap();
|
|
110
|
+
function getManifestCache(manifest) {
|
|
111
|
+
const cache = manifestCaches.get(manifest);
|
|
112
|
+
if (cache) return cache;
|
|
113
|
+
const newCache = createLRUCache(MANIFEST_CACHE_SIZE);
|
|
114
|
+
manifestCaches.set(manifest, newCache);
|
|
115
|
+
return newCache;
|
|
116
|
+
}
|
|
100
117
|
function attachRouterServerSsrUtils({
|
|
101
118
|
router,
|
|
102
119
|
manifest
|
|
@@ -109,11 +126,11 @@ function attachRouterServerSsrUtils({
|
|
|
109
126
|
const renderFinishedListeners = [];
|
|
110
127
|
const serializationFinishedListeners = [];
|
|
111
128
|
const scriptBuffer = new ScriptBuffer(router);
|
|
112
|
-
let injectedHtmlBuffer =
|
|
129
|
+
let injectedHtmlBuffer = "";
|
|
113
130
|
router.serverSsr = {
|
|
114
131
|
injectHtml: (html) => {
|
|
115
132
|
if (!html) return;
|
|
116
|
-
injectedHtmlBuffer
|
|
133
|
+
injectedHtmlBuffer += html;
|
|
117
134
|
router.emit({
|
|
118
135
|
type: "onInjectedHtml"
|
|
119
136
|
});
|
|
@@ -132,28 +149,30 @@ function attachRouterServerSsrUtils({
|
|
|
132
149
|
const matches = matchesToDehydrate.map(dehydrateMatch);
|
|
133
150
|
let manifestToDehydrate = void 0;
|
|
134
151
|
if (manifest) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
return [];
|
|
152
|
+
const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId);
|
|
153
|
+
const manifestCacheKey = currentRouteIdsList.join("\0");
|
|
154
|
+
let filteredRoutes;
|
|
155
|
+
if (isProd) {
|
|
156
|
+
filteredRoutes = getManifestCache(manifest).get(manifestCacheKey);
|
|
157
|
+
}
|
|
158
|
+
if (!filteredRoutes) {
|
|
159
|
+
const currentRouteIds = new Set(currentRouteIdsList);
|
|
160
|
+
const nextFilteredRoutes = {};
|
|
161
|
+
for (const routeId in manifest.routes) {
|
|
162
|
+
const routeManifest = manifest.routes[routeId];
|
|
163
|
+
if (currentRouteIds.has(routeId)) {
|
|
164
|
+
nextFilteredRoutes[routeId] = routeManifest;
|
|
165
|
+
} else if (routeManifest.assets && routeManifest.assets.length > 0) {
|
|
166
|
+
nextFilteredRoutes[routeId] = {
|
|
167
|
+
assets: routeManifest.assets
|
|
168
|
+
};
|
|
154
169
|
}
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
}
|
|
171
|
+
if (isProd) {
|
|
172
|
+
getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes);
|
|
173
|
+
}
|
|
174
|
+
filteredRoutes = nextFilteredRoutes;
|
|
175
|
+
}
|
|
157
176
|
manifestToDehydrate = {
|
|
158
177
|
routes: filteredRoutes
|
|
159
178
|
};
|
|
@@ -190,9 +209,9 @@ function attachRouterServerSsrUtils({
|
|
|
190
209
|
refs: /* @__PURE__ */ new Map(),
|
|
191
210
|
plugins,
|
|
192
211
|
onSerialize: (data, initial) => {
|
|
193
|
-
let serialized = initial ?
|
|
212
|
+
let serialized = initial ? TSR_PREFIX + data : data;
|
|
194
213
|
if (trackPlugins.didRun) {
|
|
195
|
-
serialized =
|
|
214
|
+
serialized = P_PREFIX + serialized + P_SUFFIX;
|
|
196
215
|
}
|
|
197
216
|
scriptBuffer.enqueue(serialized);
|
|
198
217
|
},
|
|
@@ -243,18 +262,18 @@ function attachRouterServerSsrUtils({
|
|
|
243
262
|
scriptBuffer.liftBarrier();
|
|
244
263
|
},
|
|
245
264
|
takeBufferedHtml() {
|
|
246
|
-
if (injectedHtmlBuffer
|
|
265
|
+
if (!injectedHtmlBuffer) {
|
|
247
266
|
return void 0;
|
|
248
267
|
}
|
|
249
|
-
const buffered = injectedHtmlBuffer
|
|
250
|
-
injectedHtmlBuffer =
|
|
268
|
+
const buffered = injectedHtmlBuffer;
|
|
269
|
+
injectedHtmlBuffer = "";
|
|
251
270
|
return buffered;
|
|
252
271
|
},
|
|
253
272
|
cleanup() {
|
|
254
273
|
if (!router.serverSsr) return;
|
|
255
274
|
renderFinishedListeners.length = 0;
|
|
256
275
|
serializationFinishedListeners.length = 0;
|
|
257
|
-
injectedHtmlBuffer =
|
|
276
|
+
injectedHtmlBuffer = "";
|
|
258
277
|
scriptBuffer.cleanup();
|
|
259
278
|
router.serverSsr = void 0;
|
|
260
279
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer: Array<string> = []\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer.push(html)\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n const currentRouteIds = new Set(\n router.state.matches.map((k) => k.routeId),\n )\n const filteredRoutes = Object.fromEntries(\n Object.entries(manifest.routes).flatMap(\n ([routeId, routeManifest]) => {\n if (currentRouteIds.has(routeId)) {\n return [[routeId, routeManifest]]\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n return [\n [\n routeId,\n {\n assets: routeManifest.assets,\n },\n ],\n ]\n }\n return []\n },\n ),\n )\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? GLOBAL_TSR + '.router=' + data : data\n if (trackPlugins.didRun) {\n serialized = GLOBAL_TSR + '.p(()=>' + serialized + ')'\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (injectedHtmlBuffer.length === 0) {\n return undefined\n }\n const buffered = injectedHtmlBuffer.join('')\n injectedHtmlBuffer = []\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = []\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const decodedPathname = decodePath(rawUrl.pathname)\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return new URL(normalizedHref, rawUrl.origin)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ssr-server.js","sources":["../../../src/ssr/ssr-server.ts"],"sourcesContent":["import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'\nimport invariant from 'tiny-invariant'\nimport { decodePath } from '../utils'\nimport { createLRUCache } from '../lru-cache'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { LRUCache } from '../lru-cache'\nimport type { DehydratedMatch, DehydratedRouter } from './types'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n cleanup: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n }\n onSerializationFinished: {\n type: 'onSerializationFinished'\n }\n }\n}\n\nconst SCOPE_ID = 'tsr'\n\nconst TSR_PREFIX = GLOBAL_TSR + '.router='\nconst P_PREFIX = GLOBAL_TSR + '.p(()=>'\nconst P_SUFFIX = ')'\n\nexport function dehydrateMatch(match: AnyRouteMatch): DehydratedMatch {\n const dehydratedMatch: DehydratedMatch = {\n i: match.id,\n u: match.updatedAt,\n s: match.status,\n }\n\n const properties = [\n ['__beforeLoadContext', 'b'],\n ['loaderData', 'l'],\n ['error', 'e'],\n ['ssr', 'ssr'],\n ] as const\n\n for (const [key, shorthand] of properties) {\n if (match[key] !== undefined) {\n dehydratedMatch[shorthand] = match[key]\n }\n }\n return dehydratedMatch\n}\n\nconst INITIAL_SCRIPTS = [\n getCrossReferenceHeader(SCOPE_ID),\n minifiedTsrBootStrapScript,\n]\n\nclass ScriptBuffer {\n private router: AnyRouter | undefined\n private _queue: Array<string>\n private _scriptBarrierLifted = false\n private _cleanedUp = false\n private _pendingMicrotask = false\n\n constructor(router: AnyRouter) {\n this.router = router\n // Copy INITIAL_SCRIPTS to avoid mutating the shared array\n this._queue = INITIAL_SCRIPTS.slice()\n }\n\n enqueue(script: string) {\n if (this._cleanedUp) return\n this._queue.push(script)\n // If barrier is lifted, schedule injection (if not already scheduled)\n if (this._scriptBarrierLifted && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted || this._cleanedUp) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0 && !this._pendingMicrotask) {\n this._pendingMicrotask = true\n queueMicrotask(() => {\n this._pendingMicrotask = false\n this.injectBufferedScripts()\n })\n }\n }\n\n /**\n * Flushes any pending scripts synchronously.\n * Call this before emitting onSerializationFinished to ensure all scripts are injected.\n *\n * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,\n * scripts should remain in the queue so takeBufferedScripts() can retrieve them\n */\n flush() {\n if (!this._scriptBarrierLifted) return\n if (this._cleanedUp) return\n this._pendingMicrotask = false\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n // Optimization: if only one script, avoid join\n if (bufferedScripts.length === 1) {\n return bufferedScripts[0] + ';document.currentScript.remove()'\n }\n // Append cleanup script and join - avoid push() to not mutate then iterate\n return bufferedScripts.join(';') + ';document.currentScript.remove()'\n }\n\n injectBufferedScripts() {\n if (this._cleanedUp) return\n // Early return if queue is empty (avoids unnecessary takeAll() call)\n if (this._queue.length === 0) return\n const scriptsToInject = this.takeAll()\n if (scriptsToInject && this.router?.serverSsr) {\n this.router.serverSsr.injectScript(scriptsToInject)\n }\n }\n\n cleanup() {\n this._cleanedUp = true\n this._queue = []\n this.router = undefined\n }\n}\n\nconst isProd = process.env.NODE_ENV === 'production'\n\ntype FilteredRoutes = Manifest['routes']\n\ntype ManifestLRU = LRUCache<string, FilteredRoutes>\n\nconst MANIFEST_CACHE_SIZE = 100\nconst manifestCaches = new WeakMap<Manifest, ManifestLRU>()\n\nfunction getManifestCache(manifest: Manifest): ManifestLRU {\n const cache = manifestCaches.get(manifest)\n if (cache) return cache\n const newCache = createLRUCache<string, FilteredRoutes>(MANIFEST_CACHE_SIZE)\n manifestCaches.set(manifest, newCache)\n return newCache\n}\n\nexport function attachRouterServerSsrUtils({\n router,\n manifest,\n}: {\n router: AnyRouter\n manifest: Manifest | undefined\n}) {\n router.ssr = {\n manifest,\n }\n let _dehydrated = false\n let _serializationFinished = false\n const renderFinishedListeners: Array<() => void> = []\n const serializationFinishedListeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n let injectedHtmlBuffer = ''\n\n router.serverSsr = {\n injectHtml: (html: string) => {\n if (!html) return\n // Buffer the HTML so it can be retrieved via takeBufferedHtml()\n injectedHtmlBuffer += html\n // Emit event to notify subscribers that new HTML is available\n router.emit({\n type: 'onInjectedHtml',\n })\n },\n injectScript: (script: string) => {\n if (!script) return\n const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`\n router.serverSsr!.injectHtml(html)\n },\n dehydrate: async () => {\n invariant(!_dehydrated, 'router is already dehydrated!')\n let matchesToDehydrate = router.state.matches\n if (router.isShell()) {\n // In SPA mode we only want to dehydrate the root match\n matchesToDehydrate = matchesToDehydrate.slice(0, 1)\n }\n const matches = matchesToDehydrate.map(dehydrateMatch)\n\n let manifestToDehydrate: Manifest | undefined = undefined\n // For currently matched routes, send full manifest (preloads + assets)\n // For all other routes, only send assets (no preloads as they are handled via dynamic imports)\n if (manifest) {\n // Prod-only caching; in dev manifests may be replaced/updated (HMR)\n const currentRouteIdsList = matchesToDehydrate.map((m) => m.routeId)\n const manifestCacheKey = currentRouteIdsList.join('\\0')\n\n let filteredRoutes: FilteredRoutes | undefined\n\n if (isProd) {\n filteredRoutes = getManifestCache(manifest).get(manifestCacheKey)\n }\n\n if (!filteredRoutes) {\n const currentRouteIds = new Set(currentRouteIdsList)\n const nextFilteredRoutes: FilteredRoutes = {}\n\n for (const routeId in manifest.routes) {\n const routeManifest = manifest.routes[routeId]!\n if (currentRouteIds.has(routeId)) {\n nextFilteredRoutes[routeId] = routeManifest\n } else if (\n routeManifest.assets &&\n routeManifest.assets.length > 0\n ) {\n nextFilteredRoutes[routeId] = {\n assets: routeManifest.assets,\n }\n }\n }\n\n if (isProd) {\n getManifestCache(manifest).set(manifestCacheKey, nextFilteredRoutes)\n }\n\n filteredRoutes = nextFilteredRoutes\n }\n\n manifestToDehydrate = {\n routes: filteredRoutes,\n }\n }\n const dehydratedRouter: DehydratedRouter = {\n manifest: manifestToDehydrate,\n matches,\n }\n const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id\n if (lastMatchId) {\n dehydratedRouter.lastMatchId = lastMatchId\n }\n const dehydratedData = await router.options.dehydrate?.()\n if (dehydratedData) {\n dehydratedRouter.dehydratedData = dehydratedData\n }\n _dehydrated = true\n\n const trackPlugins = { didRun: false }\n const serializationAdapters = router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n const plugins = serializationAdapters\n ? serializationAdapters\n .map((t) => makeSsrSerovalPlugin(t, trackPlugins))\n .concat(defaultSerovalPlugins)\n : defaultSerovalPlugins\n\n const signalSerializationComplete = () => {\n _serializationFinished = true\n try {\n serializationFinishedListeners.forEach((l) => l())\n router.emit({ type: 'onSerializationFinished' })\n } catch (err) {\n console.error('Serialization listener error:', err)\n } finally {\n serializationFinishedListeners.length = 0\n renderFinishedListeners.length = 0\n }\n }\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins,\n onSerialize: (data, initial) => {\n let serialized = initial ? TSR_PREFIX + data : data\n if (trackPlugins.didRun) {\n serialized = P_PREFIX + serialized + P_SUFFIX\n }\n scriptBuffer.enqueue(serialized)\n },\n scopeId: SCOPE_ID,\n onDone: () => {\n scriptBuffer.enqueue(GLOBAL_TSR + '.e()')\n // Flush all pending scripts synchronously before signaling completion\n // This ensures all scripts are injected before onSerializationFinished is emitted\n scriptBuffer.flush()\n signalSerializationComplete()\n },\n onError: (err) => {\n console.error('Serialization error:', err)\n signalSerializationComplete()\n },\n })\n },\n isDehydrated() {\n return _dehydrated\n },\n isSerializationFinished() {\n return _serializationFinished\n },\n onRenderFinished: (listener) => renderFinishedListeners.push(listener),\n onSerializationFinished: (listener) =>\n serializationFinishedListeners.push(listener),\n setRenderFinished: () => {\n // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called\n try {\n renderFinishedListeners.forEach((l) => l())\n } catch (err) {\n console.error('Error in render finished listener:', err)\n } finally {\n // Clear listeners after calling them to prevent memory leaks\n renderFinishedListeners.length = 0\n }\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n const serverBufferedScript: RouterManagedTag = {\n tag: 'script',\n attrs: {\n nonce: router.options.ssr?.nonce,\n className: '$tsr',\n id: TSR_SCRIPT_BARRIER_ID,\n },\n children: scripts,\n }\n return serverBufferedScript\n },\n liftScriptBarrier() {\n scriptBuffer.liftBarrier()\n },\n takeBufferedHtml() {\n if (!injectedHtmlBuffer) {\n return undefined\n }\n const buffered = injectedHtmlBuffer\n injectedHtmlBuffer = ''\n return buffered\n },\n cleanup() {\n // Guard against multiple cleanup calls\n if (!router.serverSsr) return\n renderFinishedListeners.length = 0\n serializationFinishedListeners.length = 0\n injectedHtmlBuffer = ''\n scriptBuffer.cleanup()\n router.serverSsr = undefined\n },\n }\n}\n\n/**\n * Get the origin for the request.\n *\n * SECURITY: We intentionally do NOT trust the Origin header for determining\n * the router's origin. The Origin header can be spoofed by attackers, which\n * could lead to SSRF-like vulnerabilities where redirects are constructed\n * using a malicious origin (CVE-2024-34351).\n *\n * Instead, we derive the origin from request.url, which is typically set by\n * the server infrastructure (not client-controlled headers).\n *\n * For applications behind proxies that need to trust forwarded headers,\n * use the router's `origin` option to explicitly configure a trusted origin.\n */\nexport function getOrigin(request: Request) {\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n\n// server and browser can decode/encode characters differently in paths and search params.\n// Server generally strictly follows the WHATWG URL Standard, while browsers may differ for legacy reasons.\n// for example, in paths \"|\" is not encoded on the server but is encoded on chromium (and not on firefox) while \"대\" is encoded on both sides.\n// Another anomaly is that in Node new URLSearchParams and new URL also decode/encode characters differently.\n// new URLSearchParams() encodes \"|\" while new URL() does not, and in this instance\n// chromium treats search params differently than paths, i.e. \"|\" is not encoded in search params.\nexport function getNormalizedURL(url: string | URL, base?: string | URL) {\n // ensure backslashes are encoded correctly in the URL\n if (typeof url === 'string') url = url.replace('\\\\', '%5C')\n\n const rawUrl = new URL(url, base)\n const decodedPathname = decodePath(rawUrl.pathname)\n const searchParams = new URLSearchParams(rawUrl.search)\n const normalizedHref =\n decodedPathname +\n (searchParams.size > 0 ? '?' : '') +\n searchParams.toString() +\n rawUrl.hash\n\n return new URL(normalizedHref, rawUrl.origin)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -51,6 +51,87 @@ function findLastClosingTagEnd(str) {
|
|
|
51
51
|
return -1;
|
|
52
52
|
}
|
|
53
53
|
function transformStreamWithRouter(router, appStream, opts) {
|
|
54
|
+
const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
|
|
55
|
+
const initialBufferedHtml = router.serverSsr?.takeBufferedHtml();
|
|
56
|
+
if (serializationAlreadyFinished && !initialBufferedHtml) {
|
|
57
|
+
let cleanedUp2 = false;
|
|
58
|
+
let controller2;
|
|
59
|
+
let isStreamClosed2 = false;
|
|
60
|
+
let lifetimeTimeoutHandle2;
|
|
61
|
+
const cleanup2 = () => {
|
|
62
|
+
if (cleanedUp2) return;
|
|
63
|
+
cleanedUp2 = true;
|
|
64
|
+
if (lifetimeTimeoutHandle2 !== void 0) {
|
|
65
|
+
clearTimeout(lifetimeTimeoutHandle2);
|
|
66
|
+
lifetimeTimeoutHandle2 = void 0;
|
|
67
|
+
}
|
|
68
|
+
router.serverSsr?.cleanup();
|
|
69
|
+
};
|
|
70
|
+
const safeClose2 = () => {
|
|
71
|
+
if (isStreamClosed2) return;
|
|
72
|
+
isStreamClosed2 = true;
|
|
73
|
+
try {
|
|
74
|
+
controller2?.close();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const safeError2 = (error) => {
|
|
79
|
+
if (isStreamClosed2) return;
|
|
80
|
+
isStreamClosed2 = true;
|
|
81
|
+
try {
|
|
82
|
+
controller2?.error(error);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const lifetimeMs2 = opts?.lifetimeMs ?? DEFAULT_LIFETIME_TIMEOUT_MS;
|
|
87
|
+
lifetimeTimeoutHandle2 = setTimeout(() => {
|
|
88
|
+
if (!cleanedUp2 && !isStreamClosed2) {
|
|
89
|
+
console.warn(
|
|
90
|
+
`SSR stream transform exceeded maximum lifetime (${lifetimeMs2}ms), forcing cleanup`
|
|
91
|
+
);
|
|
92
|
+
safeError2(new Error("Stream lifetime exceeded"));
|
|
93
|
+
cleanup2();
|
|
94
|
+
}
|
|
95
|
+
}, lifetimeMs2);
|
|
96
|
+
const stream2 = new ReadableStream({
|
|
97
|
+
start(c) {
|
|
98
|
+
controller2 = c;
|
|
99
|
+
},
|
|
100
|
+
cancel() {
|
|
101
|
+
isStreamClosed2 = true;
|
|
102
|
+
cleanup2();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
(async () => {
|
|
106
|
+
const reader = appStream.getReader();
|
|
107
|
+
try {
|
|
108
|
+
while (true) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) break;
|
|
111
|
+
if (cleanedUp2 || isStreamClosed2) return;
|
|
112
|
+
controller2?.enqueue(value);
|
|
113
|
+
}
|
|
114
|
+
if (cleanedUp2 || isStreamClosed2) return;
|
|
115
|
+
router.serverSsr?.setRenderFinished();
|
|
116
|
+
safeClose2();
|
|
117
|
+
cleanup2();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (cleanedUp2) return;
|
|
120
|
+
console.error("Error reading appStream:", error);
|
|
121
|
+
router.serverSsr?.setRenderFinished();
|
|
122
|
+
safeError2(error);
|
|
123
|
+
cleanup2();
|
|
124
|
+
} finally {
|
|
125
|
+
reader.releaseLock();
|
|
126
|
+
}
|
|
127
|
+
})().catch((error) => {
|
|
128
|
+
if (cleanedUp2) return;
|
|
129
|
+
console.error("Error in stream transform:", error);
|
|
130
|
+
safeError2(error);
|
|
131
|
+
cleanup2();
|
|
132
|
+
});
|
|
133
|
+
return stream2;
|
|
134
|
+
}
|
|
54
135
|
let stopListeningToInjectedHtml;
|
|
55
136
|
let stopListeningToSerializationFinished;
|
|
56
137
|
let serializationTimeoutHandle;
|
|
@@ -58,31 +139,14 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
58
139
|
let cleanedUp = false;
|
|
59
140
|
let controller;
|
|
60
141
|
let isStreamClosed = false;
|
|
61
|
-
const serializationAlreadyFinished = router.serverSsr?.isSerializationFinished() ?? false;
|
|
62
|
-
function cleanup() {
|
|
63
|
-
if (cleanedUp) return;
|
|
64
|
-
cleanedUp = true;
|
|
65
|
-
try {
|
|
66
|
-
stopListeningToInjectedHtml?.();
|
|
67
|
-
stopListeningToSerializationFinished?.();
|
|
68
|
-
} catch (e) {
|
|
69
|
-
}
|
|
70
|
-
stopListeningToInjectedHtml = void 0;
|
|
71
|
-
stopListeningToSerializationFinished = void 0;
|
|
72
|
-
if (serializationTimeoutHandle !== void 0) {
|
|
73
|
-
clearTimeout(serializationTimeoutHandle);
|
|
74
|
-
serializationTimeoutHandle = void 0;
|
|
75
|
-
}
|
|
76
|
-
if (lifetimeTimeoutHandle !== void 0) {
|
|
77
|
-
clearTimeout(lifetimeTimeoutHandle);
|
|
78
|
-
lifetimeTimeoutHandle = void 0;
|
|
79
|
-
}
|
|
80
|
-
pendingRouterHtmlParts = [];
|
|
81
|
-
leftover = "";
|
|
82
|
-
pendingClosingTags = "";
|
|
83
|
-
router.serverSsr?.cleanup();
|
|
84
|
-
}
|
|
85
142
|
const textDecoder = new TextDecoder();
|
|
143
|
+
let pendingRouterHtml = initialBufferedHtml ?? "";
|
|
144
|
+
let leftover = "";
|
|
145
|
+
let pendingClosingTags = "";
|
|
146
|
+
const MAX_LEFTOVER_CHARS = 2048;
|
|
147
|
+
let isAppRendering = true;
|
|
148
|
+
let streamBarrierLifted = false;
|
|
149
|
+
let serializationFinished = serializationAlreadyFinished;
|
|
86
150
|
function safeEnqueue(chunk) {
|
|
87
151
|
if (isStreamClosed) return;
|
|
88
152
|
if (typeof chunk === "string") {
|
|
@@ -107,6 +171,29 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
107
171
|
} catch {
|
|
108
172
|
}
|
|
109
173
|
}
|
|
174
|
+
function cleanup() {
|
|
175
|
+
if (cleanedUp) return;
|
|
176
|
+
cleanedUp = true;
|
|
177
|
+
try {
|
|
178
|
+
stopListeningToInjectedHtml?.();
|
|
179
|
+
stopListeningToSerializationFinished?.();
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
stopListeningToInjectedHtml = void 0;
|
|
183
|
+
stopListeningToSerializationFinished = void 0;
|
|
184
|
+
if (serializationTimeoutHandle !== void 0) {
|
|
185
|
+
clearTimeout(serializationTimeoutHandle);
|
|
186
|
+
serializationTimeoutHandle = void 0;
|
|
187
|
+
}
|
|
188
|
+
if (lifetimeTimeoutHandle !== void 0) {
|
|
189
|
+
clearTimeout(lifetimeTimeoutHandle);
|
|
190
|
+
lifetimeTimeoutHandle = void 0;
|
|
191
|
+
}
|
|
192
|
+
pendingRouterHtml = "";
|
|
193
|
+
leftover = "";
|
|
194
|
+
pendingClosingTags = "";
|
|
195
|
+
router.serverSsr?.cleanup();
|
|
196
|
+
}
|
|
110
197
|
const stream = new ReadableStream({
|
|
111
198
|
start(c) {
|
|
112
199
|
controller = c;
|
|
@@ -116,21 +203,14 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
116
203
|
cleanup();
|
|
117
204
|
}
|
|
118
205
|
});
|
|
119
|
-
let isAppRendering = true;
|
|
120
|
-
let streamBarrierLifted = false;
|
|
121
|
-
let leftover = "";
|
|
122
|
-
let pendingClosingTags = "";
|
|
123
|
-
let serializationFinished = serializationAlreadyFinished;
|
|
124
|
-
let pendingRouterHtmlParts = [];
|
|
125
|
-
const bufferedHtml = router.serverSsr?.takeBufferedHtml();
|
|
126
|
-
if (bufferedHtml) {
|
|
127
|
-
pendingRouterHtmlParts.push(bufferedHtml);
|
|
128
|
-
}
|
|
129
206
|
function flushPendingRouterHtml() {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
207
|
+
if (!pendingRouterHtml) return;
|
|
208
|
+
safeEnqueue(pendingRouterHtml);
|
|
209
|
+
pendingRouterHtml = "";
|
|
210
|
+
}
|
|
211
|
+
function appendRouterHtml(html) {
|
|
212
|
+
if (!html) return;
|
|
213
|
+
pendingRouterHtml += html;
|
|
134
214
|
}
|
|
135
215
|
function tryFinish() {
|
|
136
216
|
if (isAppRendering || !serializationFinished) return;
|
|
@@ -162,8 +242,8 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
162
242
|
if (cleanedUp || isStreamClosed) return;
|
|
163
243
|
const html = router.serverSsr?.takeBufferedHtml();
|
|
164
244
|
if (!html) return;
|
|
165
|
-
if (isAppRendering || leftover) {
|
|
166
|
-
|
|
245
|
+
if (isAppRendering || leftover || pendingClosingTags) {
|
|
246
|
+
appendRouterHtml(html);
|
|
167
247
|
} else {
|
|
168
248
|
safeEnqueue(html);
|
|
169
249
|
}
|
|
@@ -184,13 +264,18 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
184
264
|
if (done) break;
|
|
185
265
|
if (cleanedUp || isStreamClosed) return;
|
|
186
266
|
const text = value instanceof Uint8Array ? textDecoder.decode(value, { stream: true }) : String(value);
|
|
187
|
-
const chunkString = leftover + text;
|
|
267
|
+
const chunkString = leftover ? leftover + text : text;
|
|
188
268
|
if (!streamBarrierLifted) {
|
|
189
269
|
if (chunkString.includes(TSR_SCRIPT_BARRIER_ID)) {
|
|
190
270
|
streamBarrierLifted = true;
|
|
191
271
|
router.serverSsr?.liftScriptBarrier();
|
|
192
272
|
}
|
|
193
273
|
}
|
|
274
|
+
if (pendingClosingTags) {
|
|
275
|
+
pendingClosingTags += chunkString;
|
|
276
|
+
leftover = "";
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
194
279
|
const bodyEndIndex = chunkString.indexOf(BODY_END_TAG);
|
|
195
280
|
const htmlEndIndex = chunkString.indexOf(HTML_END_TAG);
|
|
196
281
|
if (bodyEndIndex !== -1 && htmlEndIndex !== -1 && bodyEndIndex < htmlEndIndex) {
|
|
@@ -205,8 +290,19 @@ function transformStreamWithRouter(router, appStream, opts) {
|
|
|
205
290
|
safeEnqueue(chunkString.slice(0, lastClosingTagEnd));
|
|
206
291
|
flushPendingRouterHtml();
|
|
207
292
|
leftover = chunkString.slice(lastClosingTagEnd);
|
|
293
|
+
if (leftover.length > MAX_LEFTOVER_CHARS) {
|
|
294
|
+
safeEnqueue(leftover.slice(0, leftover.length - MAX_LEFTOVER_CHARS));
|
|
295
|
+
leftover = leftover.slice(-MAX_LEFTOVER_CHARS);
|
|
296
|
+
}
|
|
208
297
|
} else {
|
|
209
|
-
|
|
298
|
+
const combined = chunkString;
|
|
299
|
+
if (combined.length > MAX_LEFTOVER_CHARS) {
|
|
300
|
+
const flushUpto = combined.length - MAX_LEFTOVER_CHARS;
|
|
301
|
+
safeEnqueue(combined.slice(0, flushUpto));
|
|
302
|
+
leftover = combined.slice(flushUpto);
|
|
303
|
+
} else {
|
|
304
|
+
leftover = combined;
|
|
305
|
+
}
|
|
210
306
|
}
|
|
211
307
|
}
|
|
212
308
|
if (cleanedUp || isStreamClosed) return;
|