@tanstack/router-core 1.136.8 → 1.136.11

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.
@@ -5,6 +5,7 @@ import minifiedTsrBootStrapScript from "./tsrScript.js";
5
5
  import { GLOBAL_TSR } from "./constants.js";
6
6
  import { defaultSerovalPlugins } from "./serializer/seroval-plugins.js";
7
7
  import { makeSsrSerovalPlugin } from "./serializer/transformer.js";
8
+ import { TSR_SCRIPT_BARRIER_ID } from "./transformStreamWithRouter.js";
8
9
  const SCOPE_ID = "tsr";
9
10
  function dehydrateMatch(match) {
10
11
  const dehydratedMatch = {
@@ -107,8 +108,20 @@ function attachRouterServerSsrUtils({
107
108
  matchesToDehydrate = matchesToDehydrate.slice(0, 1);
108
109
  }
109
110
  const matches = matchesToDehydrate.map(dehydrateMatch);
111
+ let manifestToDehydrate = void 0;
112
+ if (manifest) {
113
+ const filteredRoutes = Object.fromEntries(
114
+ router.state.matches.map((k) => [
115
+ k.routeId,
116
+ manifest.routes[k.routeId]
117
+ ])
118
+ );
119
+ manifestToDehydrate = {
120
+ routes: filteredRoutes
121
+ };
122
+ }
110
123
  const dehydratedRouter = {
111
- manifest: router.ssr.manifest,
124
+ manifest: manifestToDehydrate,
112
125
  matches
113
126
  };
114
127
  const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id;
@@ -152,8 +165,19 @@ function attachRouterServerSsrUtils({
152
165
  },
153
166
  takeBufferedScripts() {
154
167
  const scripts = scriptBuffer.takeAll();
168
+ const serverBufferedScript = {
169
+ tag: "script",
170
+ attrs: {
171
+ nonce: router.options.ssr?.nonce,
172
+ className: "$tsr",
173
+ id: TSR_SCRIPT_BARRIER_ID
174
+ },
175
+ children: scripts
176
+ };
177
+ return serverBufferedScript;
178
+ },
179
+ liftScriptBarrier() {
155
180
  scriptBuffer.liftBarrier();
156
- return scripts;
157
181
  }
158
182
  };
159
183
  }
@@ -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 { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest } from '../manifest'\nimport type { AnySerializationAdapter } from './serializer/transformer'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\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 constructor(private router: AnyRouter) {}\n private _queue: Array<string> = [...INITIAL_SCRIPTS]\n private _scriptBarrierLifted = false\n\n enqueue(script: string) {\n if (this._scriptBarrierLifted && this._queue.length === 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n this._queue.push(script)\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n bufferedScripts.push(`${GLOBAL_TSR}.c()`)\n const joinedScripts = bufferedScripts.join(';')\n return joinedScripts\n }\n\n injectBufferedScripts() {\n const scriptsToInject = this.takeAll()\n if (scriptsToInject) {\n this.router.serverSsr!.injectScript(() => scriptsToInject)\n }\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 const listeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n if (!script) {\n return ''\n }\n return `<script${router.options.ssr?.nonce ? `nonce='${router.options.ssr.nonce}' ` : ''} class='$tsr'>${script}</script>`\n })\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 const dehydratedRouter: DehydratedRouter = {\n manifest: router.ssr!.manifest,\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 p = createControlledPromise<string>()\n const trackPlugins = { didRun: false }\n const plugins =\n (\n router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n )?.map((t) => makeSsrSerovalPlugin(t, trackPlugins)) ?? []\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins: [...plugins, ...defaultSerovalPlugins],\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 + '.streamEnd=true')\n p.resolve('')\n },\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\n scriptBuffer.liftBarrier()\n },\n takeBufferedScripts() {\n const scripts = scriptBuffer.takeAll()\n scriptBuffer.liftBarrier()\n return scripts\n },\n }\n}\n\nexport function getOrigin(request: Request) {\n const originHeader = request.headers.get('Origin')\n if (originHeader) {\n try {\n new URL(originHeader)\n return originHeader\n } catch {}\n }\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\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 { createControlledPromise } from '../utils'\nimport minifiedTsrBootStrapScript from './tsrScript?script-string'\nimport { GLOBAL_TSR } from './constants'\nimport { defaultSerovalPlugins } from './serializer/seroval-plugins'\nimport { makeSsrSerovalPlugin } from './serializer/transformer'\nimport { TSR_SCRIPT_BARRIER_ID } from './transformStreamWithRouter'\nimport type { AnySerializationAdapter } from './serializer/transformer'\nimport type { AnyRouter } from '../router'\nimport type { DehydratedMatch } from './ssr-client'\nimport type { DehydratedRouter } from './client'\nimport type { AnyRouteMatch } from '../Matches'\nimport type { Manifest, RouterManagedTag } from '../manifest'\n\ndeclare module '../router' {\n interface ServerSsr {\n setRenderFinished: () => void\n }\n interface RouterEvents {\n onInjectedHtml: {\n type: 'onInjectedHtml'\n promise: Promise<string>\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 constructor(private router: AnyRouter) {}\n private _queue: Array<string> = [...INITIAL_SCRIPTS]\n private _scriptBarrierLifted = false\n\n enqueue(script: string) {\n if (this._scriptBarrierLifted && this._queue.length === 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n this._queue.push(script)\n }\n\n liftBarrier() {\n if (this._scriptBarrierLifted) return\n this._scriptBarrierLifted = true\n if (this._queue.length > 0) {\n queueMicrotask(() => {\n this.injectBufferedScripts()\n })\n }\n }\n\n takeAll() {\n const bufferedScripts = this._queue\n this._queue = []\n if (bufferedScripts.length === 0) {\n return undefined\n }\n bufferedScripts.push(`${GLOBAL_TSR}.c()`)\n const joinedScripts = bufferedScripts.join(';')\n return joinedScripts\n }\n\n injectBufferedScripts() {\n const scriptsToInject = this.takeAll()\n if (scriptsToInject) {\n this.router.serverSsr!.injectScript(() => scriptsToInject)\n }\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 const listeners: Array<() => void> = []\n const scriptBuffer = new ScriptBuffer(router)\n\n router.serverSsr = {\n injectedHtml: [],\n injectHtml: (getHtml) => {\n const promise = Promise.resolve().then(getHtml)\n router.serverSsr!.injectedHtml.push(promise)\n router.emit({\n type: 'onInjectedHtml',\n promise,\n })\n\n return promise.then(() => {})\n },\n injectScript: (getScript) => {\n return router.serverSsr!.injectHtml(async () => {\n const script = await getScript()\n if (!script) {\n return ''\n }\n return `<script${router.options.ssr?.nonce ? `nonce='${router.options.ssr.nonce}' ` : ''} class='$tsr'>${script}</script>`\n })\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 // only send manifest of the current routes to the client\n if (manifest) {\n const filteredRoutes = Object.fromEntries(\n router.state.matches.map((k) => [\n k.routeId,\n manifest.routes[k.routeId],\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 p = createControlledPromise<string>()\n const trackPlugins = { didRun: false }\n const plugins =\n (\n router.options.serializationAdapters as\n | Array<AnySerializationAdapter>\n | undefined\n )?.map((t) => makeSsrSerovalPlugin(t, trackPlugins)) ?? []\n\n crossSerializeStream(dehydratedRouter, {\n refs: new Map(),\n plugins: [...plugins, ...defaultSerovalPlugins],\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 + '.streamEnd=true')\n p.resolve('')\n },\n onError: (err) => p.reject(err),\n })\n // make sure the stream is kept open until the promise is resolved\n router.serverSsr!.injectHtml(() => p)\n },\n isDehydrated() {\n return _dehydrated\n },\n onRenderFinished: (listener) => listeners.push(listener),\n setRenderFinished: () => {\n listeners.forEach((l) => l())\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 }\n}\n\nexport function getOrigin(request: Request) {\n const originHeader = request.headers.get('Origin')\n if (originHeader) {\n try {\n new URL(originHeader)\n return originHeader\n } catch {}\n }\n try {\n return new URL(request.url).origin\n } catch {}\n return 'http://localhost'\n}\n"],"names":[],"mappings":";;;;;;;;AA2BA,MAAM,WAAW;AAEV,SAAS,eAAe,OAAuC;AACpE,QAAM,kBAAmC;AAAA,IACvC,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,IACT,GAAG,MAAM;AAAA,EAAA;AAGX,QAAM,aAAa;AAAA,IACjB,CAAC,uBAAuB,GAAG;AAAA,IAC3B,CAAC,cAAc,GAAG;AAAA,IAClB,CAAC,SAAS,GAAG;AAAA,IACb,CAAC,OAAO,KAAK;AAAA,EAAA;AAGf,aAAW,CAAC,KAAK,SAAS,KAAK,YAAY;AACzC,QAAI,MAAM,GAAG,MAAM,QAAW;AAC5B,sBAAgB,SAAS,IAAI,MAAM,GAAG;AAAA,IACxC;AAAA,EACF;AACA,SAAO;AACT;AAEA,MAAM,kBAAkB;AAAA,EACtB,wBAAwB,QAAQ;AAAA,EAChC;AACF;AAEA,MAAM,aAAa;AAAA,EACjB,YAAoB,QAAmB;AAAnB,SAAA,SAAA;AACpB,SAAQ,SAAwB,CAAC,GAAG,eAAe;AACnD,SAAQ,uBAAuB;AAAA,EAFS;AAAA,EAIxC,QAAQ,QAAgB;AACtB,QAAI,KAAK,wBAAwB,KAAK,OAAO,WAAW,GAAG;AACzD,qBAAe,MAAM;AACnB,aAAK,sBAAA;AAAA,MACP,CAAC;AAAA,IACH;AACA,SAAK,OAAO,KAAK,MAAM;AAAA,EACzB;AAAA,EAEA,cAAc;AACZ,QAAI,KAAK,qBAAsB;AAC/B,SAAK,uBAAuB;AAC5B,QAAI,KAAK,OAAO,SAAS,GAAG;AAC1B,qBAAe,MAAM;AACnB,aAAK,sBAAA;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAU;AACR,UAAM,kBAAkB,KAAK;AAC7B,SAAK,SAAS,CAAA;AACd,QAAI,gBAAgB,WAAW,GAAG;AAChC,aAAO;AAAA,IACT;AACA,oBAAgB,KAAK,GAAG,UAAU,MAAM;AACxC,UAAM,gBAAgB,gBAAgB,KAAK,GAAG;AAC9C,WAAO;AAAA,EACT;AAAA,EAEA,wBAAwB;AACtB,UAAM,kBAAkB,KAAK,QAAA;AAC7B,QAAI,iBAAiB;AACnB,WAAK,OAAO,UAAW,aAAa,MAAM,eAAe;AAAA,IAC3D;AAAA,EACF;AACF;AAEO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AACF,GAGG;AACD,SAAO,MAAM;AAAA,IACX;AAAA,EAAA;AAEF,MAAI,cAAc;AAClB,QAAM,YAA+B,CAAA;AACrC,QAAM,eAAe,IAAI,aAAa,MAAM;AAE5C,SAAO,YAAY;AAAA,IACjB,cAAc,CAAA;AAAA,IACd,YAAY,CAAC,YAAY;AACvB,YAAM,UAAU,QAAQ,QAAA,EAAU,KAAK,OAAO;AAC9C,aAAO,UAAW,aAAa,KAAK,OAAO;AAC3C,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN;AAAA,MAAA,CACD;AAED,aAAO,QAAQ,KAAK,MAAM;AAAA,MAAC,CAAC;AAAA,IAC9B;AAAA,IACA,cAAc,CAAC,cAAc;AAC3B,aAAO,OAAO,UAAW,WAAW,YAAY;AAC9C,cAAM,SAAS,MAAM,UAAA;AACrB,YAAI,CAAC,QAAQ;AACX,iBAAO;AAAA,QACT;AACA,eAAO,UAAU,OAAO,QAAQ,KAAK,QAAQ,UAAU,OAAO,QAAQ,IAAI,KAAK,OAAO,EAAE,iBAAiB,MAAM;AAAA,MACjH,CAAC;AAAA,IACH;AAAA,IACA,WAAW,YAAY;AACrB,gBAAU,CAAC,aAAa,+BAA+B;AACvD,UAAI,qBAAqB,OAAO,MAAM;AACtC,UAAI,OAAO,WAAW;AAEpB,6BAAqB,mBAAmB,MAAM,GAAG,CAAC;AAAA,MACpD;AACA,YAAM,UAAU,mBAAmB,IAAI,cAAc;AAErD,UAAI,sBAA4C;AAEhD,UAAI,UAAU;AACZ,cAAM,iBAAiB,OAAO;AAAA,UAC5B,OAAO,MAAM,QAAQ,IAAI,CAAC,MAAM;AAAA,YAC9B,EAAE;AAAA,YACF,SAAS,OAAO,EAAE,OAAO;AAAA,UAAA,CAC1B;AAAA,QAAA;AAEH,8BAAsB;AAAA,UACpB,QAAQ;AAAA,QAAA;AAAA,MAEZ;AACA,YAAM,mBAAqC;AAAA,QACzC,UAAU;AAAA,QACV;AAAA,MAAA;AAEF,YAAM,cAAc,mBAAmB,mBAAmB,SAAS,CAAC,GAAG;AACvE,UAAI,aAAa;AACf,yBAAiB,cAAc;AAAA,MACjC;AACA,YAAM,iBAAiB,MAAM,OAAO,QAAQ,YAAA;AAC5C,UAAI,gBAAgB;AAClB,yBAAiB,iBAAiB;AAAA,MACpC;AACA,oBAAc;AAEd,YAAM,IAAI,wBAAA;AACV,YAAM,eAAe,EAAE,QAAQ,MAAA;AAC/B,YAAM,UAEF,OAAO,QAAQ,uBAGd,IAAI,CAAC,MAAM,qBAAqB,GAAG,YAAY,CAAC,KAAK,CAAA;AAE1D,2BAAqB,kBAAkB;AAAA,QACrC,0BAAU,IAAA;AAAA,QACV,SAAS,CAAC,GAAG,SAAS,GAAG,qBAAqB;AAAA,QAC9C,aAAa,CAAC,MAAM,YAAY;AAC9B,cAAI,aAAa,UAAU,aAAa,aAAa,OAAO;AAC5D,cAAI,aAAa,QAAQ;AACvB,yBAAa,aAAa,YAAY,aAAa;AAAA,UACrD;AACA,uBAAa,QAAQ,UAAU;AAAA,QACjC;AAAA,QACA,SAAS;AAAA,QACT,QAAQ,MAAM;AACZ,uBAAa,QAAQ,aAAa,iBAAiB;AACnD,YAAE,QAAQ,EAAE;AAAA,QACd;AAAA,QACA,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG;AAAA,MAAA,CAC/B;AAED,aAAO,UAAW,WAAW,MAAM,CAAC;AAAA,IACtC;AAAA,IACA,eAAe;AACb,aAAO;AAAA,IACT;AAAA,IACA,kBAAkB,CAAC,aAAa,UAAU,KAAK,QAAQ;AAAA,IACvD,mBAAmB,MAAM;AACvB,gBAAU,QAAQ,CAAC,MAAM,EAAA,CAAG;AAC5B,mBAAa,YAAA;AAAA,IACf;AAAA,IACA,sBAAsB;AACpB,YAAM,UAAU,aAAa,QAAA;AAC7B,YAAM,uBAAyC;AAAA,QAC7C,KAAK;AAAA,QACL,OAAO;AAAA,UACL,OAAO,OAAO,QAAQ,KAAK;AAAA,UAC3B,WAAW;AAAA,UACX,IAAI;AAAA,QAAA;AAAA,QAEN,UAAU;AAAA,MAAA;AAEZ,aAAO;AAAA,IACT;AAAA,IACA,oBAAoB;AAClB,mBAAa,YAAA;AAAA,IACf;AAAA,EAAA;AAEJ;AAEO,SAAS,UAAU,SAAkB;AAC1C,QAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ;AACjD,MAAI,cAAc;AAChB,QAAI;AACF,UAAI,IAAI,YAAY;AACpB,aAAO;AAAA,IACT,QAAQ;AAAA,IAAC;AAAA,EACX;AACA,MAAI;AACF,WAAO,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,EAC9B,QAAQ;AAAA,EAAC;AACT,SAAO;AACT;"}
@@ -3,4 +3,7 @@ import { Readable } from 'node:stream';
3
3
  import { AnyRouter } from '../router.js';
4
4
  export declare function transformReadableStreamWithRouter(router: AnyRouter, routerStream: ReadableStream): ReadableStream<any>;
5
5
  export declare function transformPipeableStreamWithRouter(router: AnyRouter, routerStream: Readable): Readable;
6
- export declare function transformStreamWithRouter(router: AnyRouter, appStream: ReadableStream): ReadableStream<any>;
6
+ export declare const TSR_SCRIPT_BARRIER_ID = "$tsr-stream-barrier";
7
+ export declare function transformStreamWithRouter(router: AnyRouter, appStream: ReadableStream, opts?: {
8
+ timeoutMs?: number;
9
+ }): ReadableStream<any>;
@@ -9,28 +9,34 @@ function transformPipeableStreamWithRouter(router, routerStream) {
9
9
  transformStreamWithRouter(router, Readable.toWeb(routerStream))
10
10
  );
11
11
  }
12
- const patternBodyStart = /(<body)/;
12
+ const TSR_SCRIPT_BARRIER_ID = "$tsr-stream-barrier";
13
13
  const patternBodyEnd = /(<\/body>)/;
14
14
  const patternHtmlEnd = /(<\/html>)/;
15
- const patternHeadStart = /(<head.*?>)/;
16
15
  const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g;
17
- const textDecoder = new TextDecoder();
18
- function createPassthrough() {
16
+ function createPassthrough(onCancel) {
19
17
  let controller;
20
18
  const encoder = new TextEncoder();
21
19
  const stream = new ReadableStream({
22
20
  start(c) {
23
21
  controller = c;
22
+ },
23
+ cancel() {
24
+ res.destroyed = true;
25
+ onCancel?.();
24
26
  }
25
27
  });
26
28
  const res = {
27
29
  stream,
28
30
  write: (chunk) => {
29
- controller.enqueue(encoder.encode(chunk));
31
+ if (typeof chunk === "string") {
32
+ controller.enqueue(encoder.encode(chunk));
33
+ } else {
34
+ controller.enqueue(chunk);
35
+ }
30
36
  },
31
37
  end: (chunk) => {
32
38
  if (chunk) {
33
- controller.enqueue(encoder.encode(chunk));
39
+ res.write(chunk);
34
40
  }
35
41
  controller.close();
36
42
  res.destroyed = true;
@@ -54,13 +60,18 @@ async function readStream(stream, opts) {
54
60
  opts.onError?.(error);
55
61
  }
56
62
  }
57
- function transformStreamWithRouter(router, appStream) {
58
- const finalPassThrough = createPassthrough();
63
+ function transformStreamWithRouter(router, appStream, opts) {
64
+ let stopListeningToInjectedHtml = void 0;
65
+ let timeoutHandle;
66
+ const finalPassThrough = createPassthrough(() => {
67
+ stopListeningToInjectedHtml?.();
68
+ clearTimeout(timeoutHandle);
69
+ });
70
+ const textDecoder = new TextDecoder();
59
71
  let isAppRendering = true;
60
72
  let routerStreamBuffer = "";
61
73
  let pendingClosingTags = "";
62
- let bodyStarted = false;
63
- let headStarted = false;
74
+ let streamBarrierLifted = false;
64
75
  let leftover = "";
65
76
  let leftoverHtml = "";
66
77
  function getBufferedRouterStream() {
@@ -70,7 +81,7 @@ function transformStreamWithRouter(router, appStream) {
70
81
  }
71
82
  function decodeChunk(chunk) {
72
83
  if (chunk instanceof Uint8Array) {
73
- return textDecoder.decode(chunk);
84
+ return textDecoder.decode(chunk, { stream: true });
74
85
  }
75
86
  return String(chunk);
76
87
  }
@@ -79,16 +90,13 @@ function transformStreamWithRouter(router, appStream) {
79
90
  router.serverSsr.injectedHtml.forEach((promise) => {
80
91
  handleInjectedHtml(promise);
81
92
  });
82
- const stopListeningToInjectedHtml = router.subscribe(
83
- "onInjectedHtml",
84
- (e) => {
85
- handleInjectedHtml(e.promise);
86
- }
87
- );
93
+ stopListeningToInjectedHtml = router.subscribe("onInjectedHtml", (e) => {
94
+ handleInjectedHtml(e.promise);
95
+ });
88
96
  function handleInjectedHtml(promise) {
89
97
  processingCount++;
90
98
  promise.then((html) => {
91
- if (!bodyStarted) {
99
+ if (isAppRendering) {
92
100
  routerStreamBuffer += html;
93
101
  } else {
94
102
  finalPassThrough.write(html);
@@ -96,48 +104,33 @@ function transformStreamWithRouter(router, appStream) {
96
104
  }).catch(injectedHtmlDonePromise.reject).finally(() => {
97
105
  processingCount--;
98
106
  if (!isAppRendering && processingCount === 0) {
99
- stopListeningToInjectedHtml();
100
107
  injectedHtmlDonePromise.resolve();
101
108
  }
102
109
  });
103
110
  }
104
111
  injectedHtmlDonePromise.then(() => {
112
+ clearTimeout(timeoutHandle);
105
113
  const finalHtml = leftoverHtml + getBufferedRouterStream() + pendingClosingTags;
106
114
  finalPassThrough.end(finalHtml);
107
115
  }).catch((err) => {
108
116
  console.error("Error reading routerStream:", err);
109
117
  finalPassThrough.destroy(err);
110
- });
118
+ }).finally(() => stopListeningToInjectedHtml?.());
111
119
  readStream(appStream, {
112
120
  onData: (chunk) => {
113
121
  const text = decodeChunk(chunk.value);
114
- let chunkString = leftover + text;
122
+ const chunkString = leftover + text;
115
123
  const bodyEndMatch = chunkString.match(patternBodyEnd);
116
124
  const htmlEndMatch = chunkString.match(patternHtmlEnd);
117
- if (!bodyStarted) {
118
- const bodyStartMatch = chunkString.match(patternBodyStart);
119
- if (bodyStartMatch) {
120
- bodyStarted = true;
121
- }
122
- }
123
- if (!headStarted) {
124
- const headStartMatch = chunkString.match(patternHeadStart);
125
- if (headStartMatch) {
126
- headStarted = true;
127
- const index = headStartMatch.index;
128
- const headTag = headStartMatch[0];
129
- const remaining = chunkString.slice(index + headTag.length);
130
- finalPassThrough.write(
131
- chunkString.slice(0, index) + headTag + getBufferedRouterStream()
132
- );
133
- chunkString = remaining;
125
+ if (!streamBarrierLifted) {
126
+ const streamBarrierIdIncluded = chunkString.includes(
127
+ TSR_SCRIPT_BARRIER_ID
128
+ );
129
+ if (streamBarrierIdIncluded) {
130
+ streamBarrierLifted = true;
131
+ router.serverSsr.liftScriptBarrier();
134
132
  }
135
133
  }
136
- if (!bodyStarted) {
137
- finalPassThrough.write(chunkString);
138
- leftover = "";
139
- return;
140
- }
141
134
  if (bodyEndMatch && htmlEndMatch && bodyEndMatch.index < htmlEndMatch.index) {
142
135
  const bodyEndIndex = bodyEndMatch.index;
143
136
  pendingClosingTags = chunkString.slice(bodyEndIndex);
@@ -166,16 +159,25 @@ function transformStreamWithRouter(router, appStream) {
166
159
  router.serverSsr.setRenderFinished();
167
160
  if (processingCount === 0) {
168
161
  injectedHtmlDonePromise.resolve();
162
+ } else {
163
+ const timeoutMs = opts?.timeoutMs ?? 6e4;
164
+ timeoutHandle = setTimeout(() => {
165
+ injectedHtmlDonePromise.reject(
166
+ new Error("Injected HTML timeout after app render finished")
167
+ );
168
+ }, timeoutMs);
169
169
  }
170
170
  },
171
171
  onError: (error) => {
172
172
  console.error("Error reading appStream:", error);
173
173
  finalPassThrough.destroy(error);
174
+ injectedHtmlDonePromise.reject(error);
174
175
  }
175
176
  });
176
177
  return finalPassThrough.stream;
177
178
  }
178
179
  export {
180
+ TSR_SCRIPT_BARRIER_ID,
179
181
  transformPipeableStreamWithRouter,
180
182
  transformReadableStreamWithRouter,
181
183
  transformStreamWithRouter
@@ -1 +1 @@
1
- {"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { createControlledPromise } from '../utils'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\n// regex pattern for matching closing body and html tags\nconst patternBodyStart = /(<body)/\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\nconst patternHeadStart = /(<head.*?>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\nconst textDecoder = new TextDecoder()\n\ntype ReadablePassthrough = {\n stream: ReadableStream\n write: (chunk: string) => void\n end: (chunk?: string) => void\n destroy: (error: unknown) => void\n destroyed: boolean\n}\n\nfunction createPassthrough() {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n controller.enqueue(encoder.encode(chunk))\n },\n end: (chunk) => {\n if (chunk) {\n controller.enqueue(encoder.encode(chunk))\n }\n controller.close()\n res.destroyed = true\n },\n destroy: (error) => {\n controller.error(error)\n },\n destroyed: false,\n }\n\n return res\n}\n\nasync function readStream(\n stream: ReadableStream,\n opts: {\n onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n onEnd?: () => void\n onError?: (error: unknown) => void\n },\n) {\n try {\n const reader = stream.getReader()\n let chunk\n while (!(chunk = await reader.read()).done) {\n opts.onData?.(chunk)\n }\n opts.onEnd?.()\n } catch (error) {\n opts.onError?.(error)\n }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n) {\n const finalPassThrough = createPassthrough()\n\n let isAppRendering = true as boolean\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let bodyStarted = false as boolean\n let headStarted = false as boolean\n let leftover = ''\n let leftoverHtml = ''\n\n function getBufferedRouterStream() {\n const html = routerStreamBuffer\n routerStreamBuffer = ''\n return html\n }\n\n function decodeChunk(chunk: unknown): string {\n if (chunk instanceof Uint8Array) {\n return textDecoder.decode(chunk)\n }\n return String(chunk)\n }\n\n const injectedHtmlDonePromise = createControlledPromise<void>()\n\n let processingCount = 0\n\n // Process any already-injected HTML\n router.serverSsr!.injectedHtml.forEach((promise) => {\n handleInjectedHtml(promise)\n })\n\n // Listen for any new injected HTML\n const stopListeningToInjectedHtml = router.subscribe(\n 'onInjectedHtml',\n (e) => {\n handleInjectedHtml(e.promise)\n },\n )\n\n function handleInjectedHtml(promise: Promise<string>) {\n processingCount++\n\n promise\n .then((html) => {\n if (!bodyStarted) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch(injectedHtmlDonePromise.reject)\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n stopListeningToInjectedHtml()\n injectedHtmlDonePromise.resolve()\n }\n })\n }\n\n injectedHtmlDonePromise\n .then(() => {\n const finalHtml =\n leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\n const text = decodeChunk(chunk.value)\n\n let chunkString = leftover + text\n const bodyEndMatch = chunkString.match(patternBodyEnd)\n const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n if (!bodyStarted) {\n const bodyStartMatch = chunkString.match(patternBodyStart)\n if (bodyStartMatch) {\n bodyStarted = true\n }\n }\n\n if (!headStarted) {\n const headStartMatch = chunkString.match(patternHeadStart)\n if (headStartMatch) {\n headStarted = true\n const index = headStartMatch.index!\n const headTag = headStartMatch[0]\n const remaining = chunkString.slice(index + headTag.length)\n finalPassThrough.write(\n chunkString.slice(0, index) + headTag + getBufferedRouterStream(),\n )\n // make sure to only write `remaining` until the next closing tag\n chunkString = remaining\n }\n }\n\n if (!bodyStarted) {\n finalPassThrough.write(chunkString)\n leftover = ''\n return\n }\n\n // If either the body end or html end is in the chunk,\n // We need to get all of our data in asap\n if (\n bodyEndMatch &&\n htmlEndMatch &&\n bodyEndMatch.index! < htmlEndMatch.index!\n ) {\n const bodyEndIndex = bodyEndMatch.index!\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n finalPassThrough.write(\n chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),\n )\n\n leftover = ''\n return\n }\n\n let result: RegExpExecArray | null\n let lastIndex = 0\n while ((result = patternClosingTag.exec(chunkString)) !== null) {\n lastIndex = result.index + result[0].length\n }\n\n if (lastIndex > 0) {\n const processed =\n chunkString.slice(0, lastIndex) +\n getBufferedRouterStream() +\n leftoverHtml\n\n finalPassThrough.write(processed)\n leftover = chunkString.slice(lastIndex)\n } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n\n // If there are no pending promises, resolve the injectedHtmlDonePromise\n if (processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n },\n onError: (error) => {\n console.error('Error reading appStream:', error)\n finalPassThrough.destroy(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transformStreamWithRouter.js","sources":["../../../src/ssr/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { createControlledPromise } from '../utils'\nimport type { AnyRouter } from '../router'\n\nexport function transformReadableStreamWithRouter(\n router: AnyRouter,\n routerStream: ReadableStream,\n) {\n return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n router: AnyRouter,\n routerStream: Readable,\n) {\n return Readable.fromWeb(\n transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n )\n}\n\nexport const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'\n\n// regex pattern for matching closing body and html tags\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\ntype ReadablePassthrough = {\n stream: ReadableStream\n write: (chunk: unknown) => void\n end: (chunk?: string) => void\n destroy: (error: unknown) => void\n destroyed: boolean\n}\n\nfunction createPassthrough(onCancel?: () => void) {\n let controller: ReadableStreamDefaultController<any>\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(c) {\n controller = c\n },\n cancel() {\n res.destroyed = true\n onCancel?.()\n },\n })\n\n const res: ReadablePassthrough = {\n stream,\n write: (chunk) => {\n if (typeof chunk === 'string') {\n controller.enqueue(encoder.encode(chunk))\n } else {\n controller.enqueue(chunk)\n }\n },\n end: (chunk) => {\n if (chunk) {\n res.write(chunk)\n }\n controller.close()\n res.destroyed = true\n },\n destroy: (error) => {\n controller.error(error)\n },\n destroyed: false,\n }\n\n return res\n}\n\nasync function readStream(\n stream: ReadableStream,\n opts: {\n onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n onEnd?: () => void\n onError?: (error: unknown) => void\n },\n) {\n try {\n const reader = stream.getReader()\n let chunk\n while (!(chunk = await reader.read()).done) {\n opts.onData?.(chunk)\n }\n opts.onEnd?.()\n } catch (error) {\n opts.onError?.(error)\n }\n}\n\nexport function transformStreamWithRouter(\n router: AnyRouter,\n appStream: ReadableStream,\n opts?: {\n timeoutMs?: number\n },\n) {\n let stopListeningToInjectedHtml: (() => void) | undefined = undefined\n let timeoutHandle: NodeJS.Timeout\n\n const finalPassThrough = createPassthrough(() => {\n stopListeningToInjectedHtml?.()\n clearTimeout(timeoutHandle)\n })\n const textDecoder = new TextDecoder()\n\n let isAppRendering = true as boolean\n let routerStreamBuffer = ''\n let pendingClosingTags = ''\n let streamBarrierLifted = false as boolean\n let leftover = ''\n let leftoverHtml = ''\n\n function getBufferedRouterStream() {\n const html = routerStreamBuffer\n routerStreamBuffer = ''\n return html\n }\n\n function decodeChunk(chunk: unknown): string {\n if (chunk instanceof Uint8Array) {\n return textDecoder.decode(chunk, { stream: true })\n }\n return String(chunk)\n }\n\n const injectedHtmlDonePromise = createControlledPromise<void>()\n\n let processingCount = 0\n\n // Process any already-injected HTML\n router.serverSsr!.injectedHtml.forEach((promise) => {\n handleInjectedHtml(promise)\n })\n\n // Listen for any new injected HTML\n stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', (e) => {\n handleInjectedHtml(e.promise)\n })\n\n function handleInjectedHtml(promise: Promise<string>) {\n processingCount++\n\n promise\n .then((html) => {\n if (isAppRendering) {\n routerStreamBuffer += html\n } else {\n finalPassThrough.write(html)\n }\n })\n .catch(injectedHtmlDonePromise.reject)\n .finally(() => {\n processingCount--\n\n if (!isAppRendering && processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n }\n })\n }\n\n injectedHtmlDonePromise\n .then(() => {\n clearTimeout(timeoutHandle)\n const finalHtml =\n leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n finalPassThrough.end(finalHtml)\n })\n .catch((err) => {\n console.error('Error reading routerStream:', err)\n finalPassThrough.destroy(err)\n })\n .finally(() => stopListeningToInjectedHtml?.())\n\n // Transform the appStream\n readStream(appStream, {\n onData: (chunk) => {\n const text = decodeChunk(chunk.value)\n const chunkString = leftover + text\n const bodyEndMatch = chunkString.match(patternBodyEnd)\n const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n if (!streamBarrierLifted) {\n const streamBarrierIdIncluded = chunkString.includes(\n TSR_SCRIPT_BARRIER_ID,\n )\n if (streamBarrierIdIncluded) {\n streamBarrierLifted = true\n router.serverSsr!.liftScriptBarrier()\n }\n }\n\n // If either the body end or html end is in the chunk,\n // We need to get all of our data in asap\n if (\n bodyEndMatch &&\n htmlEndMatch &&\n bodyEndMatch.index! < htmlEndMatch.index!\n ) {\n const bodyEndIndex = bodyEndMatch.index!\n pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n finalPassThrough.write(\n chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),\n )\n\n leftover = ''\n return\n }\n\n let result: RegExpExecArray | null\n let lastIndex = 0\n while ((result = patternClosingTag.exec(chunkString)) !== null) {\n lastIndex = result.index + result[0].length\n }\n\n if (lastIndex > 0) {\n const processed =\n chunkString.slice(0, lastIndex) +\n getBufferedRouterStream() +\n leftoverHtml\n\n finalPassThrough.write(processed)\n leftover = chunkString.slice(lastIndex)\n } else {\n leftover = chunkString\n leftoverHtml += getBufferedRouterStream()\n }\n },\n onEnd: () => {\n // Mark the app as done rendering\n isAppRendering = false\n router.serverSsr!.setRenderFinished()\n\n // If there are no pending promises, resolve the injectedHtmlDonePromise\n if (processingCount === 0) {\n injectedHtmlDonePromise.resolve()\n } else {\n const timeoutMs = opts?.timeoutMs ?? 60000\n timeoutHandle = setTimeout(() => {\n injectedHtmlDonePromise.reject(\n new Error('Injected HTML timeout after app render finished'),\n )\n }, timeoutMs)\n }\n },\n onError: (error) => {\n console.error('Error reading appStream:', error)\n finalPassThrough.destroy(error)\n injectedHtmlDonePromise.reject(error)\n },\n })\n\n return finalPassThrough.stream\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.136.8",
3
+ "version": "1.136.11",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/router.ts CHANGED
@@ -83,7 +83,7 @@ import type {
83
83
  CommitLocationOptions,
84
84
  NavigateFn,
85
85
  } from './RouterProvider'
86
- import type { Manifest } from './manifest'
86
+ import type { Manifest, RouterManagedTag } from './manifest'
87
87
  import type { AnySchema, AnyValidator } from './validators'
88
88
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
89
89
  import type { NotFoundError } from './not-found'
@@ -756,7 +756,8 @@ export interface ServerSsr {
756
756
  isDehydrated: () => boolean
757
757
  onRenderFinished: (listener: () => void) => void
758
758
  dehydrate: () => Promise<void>
759
- takeBufferedScripts: () => string | undefined
759
+ takeBufferedScripts: () => RouterManagedTag | undefined
760
+ liftScriptBarrier: () => void
760
761
  }
761
762
 
762
763
  export type AnyRouterWithContext<TContext> = RouterCore<
@@ -2096,7 +2097,6 @@ export class RouterCore<
2096
2097
  updateMatch: this.updateMatch,
2097
2098
  // eslint-disable-next-line @typescript-eslint/require-await
2098
2099
  onReady: async () => {
2099
- // eslint-disable-next-line @typescript-eslint/require-await
2100
2100
  // Wrap batch in framework-specific transition wrapper (e.g., Solid's startTransition)
2101
2101
  this.startTransition(() => {
2102
2102
  this.startViewTransition(async () => {
@@ -5,12 +5,13 @@ import minifiedTsrBootStrapScript from './tsrScript?script-string'
5
5
  import { GLOBAL_TSR } from './constants'
6
6
  import { defaultSerovalPlugins } from './serializer/seroval-plugins'
7
7
  import { makeSsrSerovalPlugin } from './serializer/transformer'
8
+ import { TSR_SCRIPT_BARRIER_ID } from './transformStreamWithRouter'
9
+ import type { AnySerializationAdapter } from './serializer/transformer'
8
10
  import type { AnyRouter } from '../router'
9
11
  import type { DehydratedMatch } from './ssr-client'
10
12
  import type { DehydratedRouter } from './client'
11
13
  import type { AnyRouteMatch } from '../Matches'
12
- import type { Manifest } from '../manifest'
13
- import type { AnySerializationAdapter } from './serializer/transformer'
14
+ import type { Manifest, RouterManagedTag } from '../manifest'
14
15
 
15
16
  declare module '../router' {
16
17
  interface ServerSsr {
@@ -140,8 +141,21 @@ export function attachRouterServerSsrUtils({
140
141
  }
141
142
  const matches = matchesToDehydrate.map(dehydrateMatch)
142
143
 
144
+ let manifestToDehydrate: Manifest | undefined = undefined
145
+ // only send manifest of the current routes to the client
146
+ if (manifest) {
147
+ const filteredRoutes = Object.fromEntries(
148
+ router.state.matches.map((k) => [
149
+ k.routeId,
150
+ manifest.routes[k.routeId],
151
+ ]),
152
+ )
153
+ manifestToDehydrate = {
154
+ routes: filteredRoutes,
155
+ }
156
+ }
143
157
  const dehydratedRouter: DehydratedRouter = {
144
- manifest: router.ssr!.manifest,
158
+ manifest: manifestToDehydrate,
145
159
  matches,
146
160
  }
147
161
  const lastMatchId = matchesToDehydrate[matchesToDehydrate.length - 1]?.id
@@ -193,8 +207,19 @@ export function attachRouterServerSsrUtils({
193
207
  },
194
208
  takeBufferedScripts() {
195
209
  const scripts = scriptBuffer.takeAll()
210
+ const serverBufferedScript: RouterManagedTag = {
211
+ tag: 'script',
212
+ attrs: {
213
+ nonce: router.options.ssr?.nonce,
214
+ className: '$tsr',
215
+ id: TSR_SCRIPT_BARRIER_ID,
216
+ },
217
+ children: scripts,
218
+ }
219
+ return serverBufferedScript
220
+ },
221
+ liftScriptBarrier() {
196
222
  scriptBuffer.liftBarrier()
197
- return scripts
198
223
  },
199
224
  }
200
225
  }
@@ -19,41 +19,47 @@ export function transformPipeableStreamWithRouter(
19
19
  )
20
20
  }
21
21
 
22
+ export const TSR_SCRIPT_BARRIER_ID = '$tsr-stream-barrier'
23
+
22
24
  // regex pattern for matching closing body and html tags
23
- const patternBodyStart = /(<body)/
24
25
  const patternBodyEnd = /(<\/body>)/
25
26
  const patternHtmlEnd = /(<\/html>)/
26
- const patternHeadStart = /(<head.*?>)/
27
27
  // regex pattern for matching closing tags
28
28
  const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g
29
29
 
30
- const textDecoder = new TextDecoder()
31
-
32
30
  type ReadablePassthrough = {
33
31
  stream: ReadableStream
34
- write: (chunk: string) => void
32
+ write: (chunk: unknown) => void
35
33
  end: (chunk?: string) => void
36
34
  destroy: (error: unknown) => void
37
35
  destroyed: boolean
38
36
  }
39
37
 
40
- function createPassthrough() {
38
+ function createPassthrough(onCancel?: () => void) {
41
39
  let controller: ReadableStreamDefaultController<any>
42
40
  const encoder = new TextEncoder()
43
41
  const stream = new ReadableStream({
44
42
  start(c) {
45
43
  controller = c
46
44
  },
45
+ cancel() {
46
+ res.destroyed = true
47
+ onCancel?.()
48
+ },
47
49
  })
48
50
 
49
51
  const res: ReadablePassthrough = {
50
52
  stream,
51
53
  write: (chunk) => {
52
- controller.enqueue(encoder.encode(chunk))
54
+ if (typeof chunk === 'string') {
55
+ controller.enqueue(encoder.encode(chunk))
56
+ } else {
57
+ controller.enqueue(chunk)
58
+ }
53
59
  },
54
60
  end: (chunk) => {
55
61
  if (chunk) {
56
- controller.enqueue(encoder.encode(chunk))
62
+ res.write(chunk)
57
63
  }
58
64
  controller.close()
59
65
  res.destroyed = true
@@ -90,14 +96,23 @@ async function readStream(
90
96
  export function transformStreamWithRouter(
91
97
  router: AnyRouter,
92
98
  appStream: ReadableStream,
99
+ opts?: {
100
+ timeoutMs?: number
101
+ },
93
102
  ) {
94
- const finalPassThrough = createPassthrough()
103
+ let stopListeningToInjectedHtml: (() => void) | undefined = undefined
104
+ let timeoutHandle: NodeJS.Timeout
105
+
106
+ const finalPassThrough = createPassthrough(() => {
107
+ stopListeningToInjectedHtml?.()
108
+ clearTimeout(timeoutHandle)
109
+ })
110
+ const textDecoder = new TextDecoder()
95
111
 
96
112
  let isAppRendering = true as boolean
97
113
  let routerStreamBuffer = ''
98
114
  let pendingClosingTags = ''
99
- let bodyStarted = false as boolean
100
- let headStarted = false as boolean
115
+ let streamBarrierLifted = false as boolean
101
116
  let leftover = ''
102
117
  let leftoverHtml = ''
103
118
 
@@ -109,7 +124,7 @@ export function transformStreamWithRouter(
109
124
 
110
125
  function decodeChunk(chunk: unknown): string {
111
126
  if (chunk instanceof Uint8Array) {
112
- return textDecoder.decode(chunk)
127
+ return textDecoder.decode(chunk, { stream: true })
113
128
  }
114
129
  return String(chunk)
115
130
  }
@@ -124,19 +139,16 @@ export function transformStreamWithRouter(
124
139
  })
125
140
 
126
141
  // Listen for any new injected HTML
127
- const stopListeningToInjectedHtml = router.subscribe(
128
- 'onInjectedHtml',
129
- (e) => {
130
- handleInjectedHtml(e.promise)
131
- },
132
- )
142
+ stopListeningToInjectedHtml = router.subscribe('onInjectedHtml', (e) => {
143
+ handleInjectedHtml(e.promise)
144
+ })
133
145
 
134
146
  function handleInjectedHtml(promise: Promise<string>) {
135
147
  processingCount++
136
148
 
137
149
  promise
138
150
  .then((html) => {
139
- if (!bodyStarted) {
151
+ if (isAppRendering) {
140
152
  routerStreamBuffer += html
141
153
  } else {
142
154
  finalPassThrough.write(html)
@@ -147,7 +159,6 @@ export function transformStreamWithRouter(
147
159
  processingCount--
148
160
 
149
161
  if (!isAppRendering && processingCount === 0) {
150
- stopListeningToInjectedHtml()
151
162
  injectedHtmlDonePromise.resolve()
152
163
  }
153
164
  })
@@ -155,6 +166,7 @@ export function transformStreamWithRouter(
155
166
 
156
167
  injectedHtmlDonePromise
157
168
  .then(() => {
169
+ clearTimeout(timeoutHandle)
158
170
  const finalHtml =
159
171
  leftoverHtml + getBufferedRouterStream() + pendingClosingTags
160
172
 
@@ -164,44 +176,26 @@ export function transformStreamWithRouter(
164
176
  console.error('Error reading routerStream:', err)
165
177
  finalPassThrough.destroy(err)
166
178
  })
179
+ .finally(() => stopListeningToInjectedHtml?.())
167
180
 
168
181
  // Transform the appStream
169
182
  readStream(appStream, {
170
183
  onData: (chunk) => {
171
184
  const text = decodeChunk(chunk.value)
172
-
173
- let chunkString = leftover + text
185
+ const chunkString = leftover + text
174
186
  const bodyEndMatch = chunkString.match(patternBodyEnd)
175
187
  const htmlEndMatch = chunkString.match(patternHtmlEnd)
176
188
 
177
- if (!bodyStarted) {
178
- const bodyStartMatch = chunkString.match(patternBodyStart)
179
- if (bodyStartMatch) {
180
- bodyStarted = true
181
- }
182
- }
183
-
184
- if (!headStarted) {
185
- const headStartMatch = chunkString.match(patternHeadStart)
186
- if (headStartMatch) {
187
- headStarted = true
188
- const index = headStartMatch.index!
189
- const headTag = headStartMatch[0]
190
- const remaining = chunkString.slice(index + headTag.length)
191
- finalPassThrough.write(
192
- chunkString.slice(0, index) + headTag + getBufferedRouterStream(),
193
- )
194
- // make sure to only write `remaining` until the next closing tag
195
- chunkString = remaining
189
+ if (!streamBarrierLifted) {
190
+ const streamBarrierIdIncluded = chunkString.includes(
191
+ TSR_SCRIPT_BARRIER_ID,
192
+ )
193
+ if (streamBarrierIdIncluded) {
194
+ streamBarrierLifted = true
195
+ router.serverSsr!.liftScriptBarrier()
196
196
  }
197
197
  }
198
198
 
199
- if (!bodyStarted) {
200
- finalPassThrough.write(chunkString)
201
- leftover = ''
202
- return
203
- }
204
-
205
199
  // If either the body end or html end is in the chunk,
206
200
  // We need to get all of our data in asap
207
201
  if (
@@ -247,11 +241,19 @@ export function transformStreamWithRouter(
247
241
  // If there are no pending promises, resolve the injectedHtmlDonePromise
248
242
  if (processingCount === 0) {
249
243
  injectedHtmlDonePromise.resolve()
244
+ } else {
245
+ const timeoutMs = opts?.timeoutMs ?? 60000
246
+ timeoutHandle = setTimeout(() => {
247
+ injectedHtmlDonePromise.reject(
248
+ new Error('Injected HTML timeout after app render finished'),
249
+ )
250
+ }, timeoutMs)
250
251
  }
251
252
  },
252
253
  onError: (error) => {
253
254
  console.error('Error reading appStream:', error)
254
255
  finalPassThrough.destroy(error)
256
+ injectedHtmlDonePromise.reject(error)
255
257
  },
256
258
  })
257
259