@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.
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +3 -2
- package/dist/cjs/ssr/ssr-server.cjs +26 -2
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +45 -43
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +4 -1
- package/dist/esm/router.d.ts +3 -2
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +26 -2
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +4 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +45 -43
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +3 -3
- package/src/ssr/ssr-server.ts +29 -4
- package/src/ssr/transformStreamWithRouter.ts +49 -47
|
@@ -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:
|
|
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":";;;;;;;AA0BA,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,YAAM,mBAAqC;AAAA,QACzC,UAAU,OAAO,IAAK;AAAA,QACtB;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,mBAAa,YAAA;AACb,aAAO;AAAA,IACT;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;"}
|
|
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":"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"}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 (
|
|
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
|
-
|
|
122
|
+
const chunkString = leftover + text;
|
|
115
123
|
const bodyEndMatch = chunkString.match(patternBodyEnd);
|
|
116
124
|
const htmlEndMatch = chunkString.match(patternHtmlEnd);
|
|
117
|
-
if (!
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
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: () =>
|
|
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 () => {
|
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 (
|
|
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 (!
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|