@tanstack/router-core 1.121.34 → 1.121.39

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.
Files changed (87) hide show
  1. package/dist/cjs/index.d.cts +1 -1
  2. package/dist/cjs/router.cjs.map +1 -1
  3. package/dist/cjs/router.d.cts +2 -2
  4. package/dist/cjs/serializer.cjs +146 -0
  5. package/dist/cjs/serializer.cjs.map +1 -0
  6. package/dist/cjs/serializer.d.cts +7 -1
  7. package/dist/cjs/ssr/client.cjs +12 -0
  8. package/dist/cjs/ssr/client.cjs.map +1 -0
  9. package/dist/cjs/ssr/client.d.cts +6 -0
  10. package/dist/cjs/ssr/createRequestHandler.cjs +50 -0
  11. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -0
  12. package/dist/cjs/ssr/createRequestHandler.d.cts +9 -0
  13. package/dist/cjs/ssr/handlerCallback.cjs +7 -0
  14. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -0
  15. package/dist/cjs/ssr/handlerCallback.d.cts +9 -0
  16. package/dist/cjs/ssr/headers.cjs +39 -0
  17. package/dist/cjs/ssr/headers.cjs.map +1 -0
  18. package/dist/cjs/ssr/headers.d.cts +5 -0
  19. package/dist/cjs/ssr/json.cjs +14 -0
  20. package/dist/cjs/ssr/json.cjs.map +1 -0
  21. package/dist/cjs/ssr/json.d.cts +4 -0
  22. package/dist/cjs/ssr/server.cjs +17 -0
  23. package/dist/cjs/ssr/server.cjs.map +1 -0
  24. package/dist/cjs/ssr/server.d.cts +8 -0
  25. package/dist/cjs/ssr/ssr-client.cjs +131 -0
  26. package/dist/cjs/ssr/ssr-client.cjs.map +1 -0
  27. package/dist/cjs/ssr/ssr-client.d.cts +68 -0
  28. package/dist/cjs/ssr/ssr-server.cjs +248 -0
  29. package/dist/cjs/ssr/ssr-server.cjs.map +1 -0
  30. package/dist/cjs/ssr/ssr-server.d.cts +32 -0
  31. package/dist/cjs/ssr/transformStreamWithRouter.cjs +183 -0
  32. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -0
  33. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +6 -0
  34. package/dist/cjs/ssr/tsrScript.cjs +4 -0
  35. package/dist/cjs/ssr/tsrScript.cjs.map +1 -0
  36. package/dist/cjs/ssr/tsrScript.d.cts +1 -0
  37. package/dist/esm/index.d.ts +1 -1
  38. package/dist/esm/router.d.ts +2 -2
  39. package/dist/esm/router.js.map +1 -1
  40. package/dist/esm/serializer.d.ts +7 -1
  41. package/dist/esm/serializer.js +146 -0
  42. package/dist/esm/serializer.js.map +1 -0
  43. package/dist/esm/ssr/client.d.ts +6 -0
  44. package/dist/esm/ssr/client.js +12 -0
  45. package/dist/esm/ssr/client.js.map +1 -0
  46. package/dist/esm/ssr/createRequestHandler.d.ts +9 -0
  47. package/dist/esm/ssr/createRequestHandler.js +50 -0
  48. package/dist/esm/ssr/createRequestHandler.js.map +1 -0
  49. package/dist/esm/ssr/handlerCallback.d.ts +9 -0
  50. package/dist/esm/ssr/handlerCallback.js +7 -0
  51. package/dist/esm/ssr/handlerCallback.js.map +1 -0
  52. package/dist/esm/ssr/headers.d.ts +5 -0
  53. package/dist/esm/ssr/headers.js +39 -0
  54. package/dist/esm/ssr/headers.js.map +1 -0
  55. package/dist/esm/ssr/json.d.ts +4 -0
  56. package/dist/esm/ssr/json.js +14 -0
  57. package/dist/esm/ssr/json.js.map +1 -0
  58. package/dist/esm/ssr/server.d.ts +8 -0
  59. package/dist/esm/ssr/server.js +17 -0
  60. package/dist/esm/ssr/server.js.map +1 -0
  61. package/dist/esm/ssr/ssr-client.d.ts +68 -0
  62. package/dist/esm/ssr/ssr-client.js +131 -0
  63. package/dist/esm/ssr/ssr-client.js.map +1 -0
  64. package/dist/esm/ssr/ssr-server.d.ts +32 -0
  65. package/dist/esm/ssr/ssr-server.js +248 -0
  66. package/dist/esm/ssr/ssr-server.js.map +1 -0
  67. package/dist/esm/ssr/transformStreamWithRouter.d.ts +6 -0
  68. package/dist/esm/ssr/transformStreamWithRouter.js +183 -0
  69. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -0
  70. package/dist/esm/ssr/tsrScript.d.ts +1 -0
  71. package/dist/esm/ssr/tsrScript.js +5 -0
  72. package/dist/esm/ssr/tsrScript.js.map +1 -0
  73. package/package.json +28 -1
  74. package/src/index.ts +1 -0
  75. package/src/router.ts +2 -2
  76. package/src/serializer.ts +174 -1
  77. package/src/ssr/client.ts +15 -0
  78. package/src/ssr/createRequestHandler.ts +74 -0
  79. package/src/ssr/handlerCallback.ts +15 -0
  80. package/src/ssr/headers.ts +51 -0
  81. package/src/ssr/json.ts +18 -0
  82. package/src/ssr/server.ts +23 -0
  83. package/src/ssr/ssr-client.ts +244 -0
  84. package/src/ssr/ssr-server.ts +345 -0
  85. package/src/ssr/transformStreamWithRouter.ts +258 -0
  86. package/src/ssr/tsrScript.ts +91 -0
  87. package/src/vite-env.d.ts +4 -0
@@ -0,0 +1,345 @@
1
+ import { default as warning } from 'tiny-warning'
2
+ import jsesc from 'jsesc'
3
+ import { TSR_DEFERRED_PROMISE, defer } from '../defer'
4
+ import { isPlainArray, isPlainObject, pick } from '../utils'
5
+ import { tsrSerializer } from '../serializer'
6
+ import minifiedTsrBootStrapScript from './tsrScript?script-string'
7
+ import type { DeferredPromise } from '../defer'
8
+ import type {
9
+ ClientExtractedBaseEntry,
10
+ DehydratedRouter,
11
+ ResolvePromiseState,
12
+ SsrMatch,
13
+ } from './ssr-client'
14
+ import type { AnyRouter } from '../router'
15
+ import type { Manifest } from '../manifest'
16
+ import type { AnyRouteMatch } from '../Matches'
17
+
18
+ export type ServerExtractedEntry =
19
+ | ServerExtractedStream
20
+ | ServerExtractedPromise
21
+
22
+ export interface ServerExtractedBaseEntry extends ClientExtractedBaseEntry {
23
+ id: number
24
+ matchIndex: number
25
+ }
26
+
27
+ export interface ServerExtractedStream extends ServerExtractedBaseEntry {
28
+ type: 'stream'
29
+ stream: ReadableStream
30
+ }
31
+
32
+ export interface ServerExtractedPromise extends ServerExtractedBaseEntry {
33
+ type: 'promise'
34
+ promise: DeferredPromise<any>
35
+ }
36
+
37
+ export function attachRouterServerSsrUtils(
38
+ router: AnyRouter,
39
+ manifest: Manifest | undefined,
40
+ ) {
41
+ router.ssr = {
42
+ manifest,
43
+ serializer: tsrSerializer,
44
+ }
45
+
46
+ router.serverSsr = {
47
+ injectedHtml: [],
48
+ streamedKeys: new Set(),
49
+ injectHtml: (getHtml) => {
50
+ const promise = Promise.resolve().then(getHtml)
51
+ router.serverSsr!.injectedHtml.push(promise)
52
+ router.emit({
53
+ type: 'onInjectedHtml',
54
+ promise,
55
+ })
56
+
57
+ return promise.then(() => {})
58
+ },
59
+ injectScript: (getScript, opts) => {
60
+ return router.serverSsr!.injectHtml(async () => {
61
+ const script = await getScript()
62
+ return `<script class='tsr-once'>${script}${
63
+ process.env.NODE_ENV === 'development' && (opts?.logScript ?? true)
64
+ ? `; console.info(\`Injected From Server:
65
+ ${jsesc(script, { quotes: 'backtick' })}\`)`
66
+ : ''
67
+ }; if (typeof __TSR_SSR__ !== 'undefined') __TSR_SSR__.cleanScripts()</script>`
68
+ })
69
+ },
70
+ streamValue: (key, value) => {
71
+ warning(
72
+ !router.serverSsr!.streamedKeys.has(key),
73
+ 'Key has already been streamed: ' + key,
74
+ )
75
+
76
+ router.serverSsr!.streamedKeys.add(key)
77
+ router.serverSsr!.injectScript(
78
+ () =>
79
+ `__TSR_SSR__.streamedValues['${key}'] = { value: ${jsesc(
80
+ router.ssr!.serializer.stringify(value),
81
+ {
82
+ isScriptContext: true,
83
+ wrap: true,
84
+ json: true,
85
+ },
86
+ )}}`,
87
+ )
88
+ },
89
+ onMatchSettled,
90
+ }
91
+
92
+ router.serverSsr.injectScript(() => minifiedTsrBootStrapScript, {
93
+ logScript: false,
94
+ })
95
+ }
96
+
97
+ export function dehydrateRouter(router: AnyRouter) {
98
+ const dehydratedRouter: DehydratedRouter = {
99
+ manifest: router.ssr!.manifest,
100
+ dehydratedData: router.options.dehydrate?.(),
101
+ lastMatchId:
102
+ router.state.matches[router.state.matches.length - 1]?.id || '',
103
+ }
104
+
105
+ router.serverSsr!.injectScript(
106
+ () =>
107
+ `__TSR_SSR__.dehydrated = ${jsesc(
108
+ router.ssr!.serializer.stringify(dehydratedRouter),
109
+ {
110
+ isScriptContext: true,
111
+ wrap: true,
112
+ json: true,
113
+ },
114
+ )}`,
115
+ )
116
+ }
117
+
118
+ export function extractAsyncLoaderData(
119
+ loaderData: any,
120
+ ctx: {
121
+ match: AnyRouteMatch
122
+ router: AnyRouter
123
+ },
124
+ ) {
125
+ const extracted: Array<ServerExtractedEntry> = []
126
+
127
+ const replaced = replaceBy(loaderData, (value, path) => {
128
+ // If it's a stream, we need to tee it so we can read it multiple times
129
+ if (value instanceof ReadableStream) {
130
+ const [copy1, copy2] = value.tee()
131
+ const entry: ServerExtractedStream = {
132
+ type: 'stream',
133
+ path,
134
+ id: extracted.length,
135
+ matchIndex: ctx.match.index,
136
+ stream: copy2,
137
+ }
138
+
139
+ extracted.push(entry)
140
+ return copy1
141
+ } else if (value instanceof Promise) {
142
+ const deferredPromise = defer(value)
143
+ const entry: ServerExtractedPromise = {
144
+ type: 'promise',
145
+ path,
146
+ id: extracted.length,
147
+ matchIndex: ctx.match.index,
148
+ promise: deferredPromise,
149
+ }
150
+ extracted.push(entry)
151
+ }
152
+
153
+ return value
154
+ })
155
+
156
+ return { replaced, extracted }
157
+ }
158
+
159
+ export function onMatchSettled(opts: {
160
+ router: AnyRouter
161
+ match: AnyRouteMatch
162
+ }) {
163
+ const { router, match } = opts
164
+
165
+ let extracted: Array<ServerExtractedEntry> | undefined = undefined
166
+ let serializedLoaderData: any = undefined
167
+ if (match.loaderData !== undefined) {
168
+ const result = extractAsyncLoaderData(match.loaderData, {
169
+ router,
170
+ match,
171
+ })
172
+ match.loaderData = result.replaced
173
+ extracted = result.extracted
174
+ serializedLoaderData = extracted.reduce(
175
+ (acc: any, entry: ServerExtractedEntry) => {
176
+ return deepImmutableSetByPath(acc, ['temp', ...entry.path], undefined)
177
+ },
178
+ { temp: result.replaced },
179
+ ).temp
180
+ }
181
+
182
+ const initCode = `__TSR_SSR__.initMatch(${jsesc(
183
+ {
184
+ id: match.id,
185
+ __beforeLoadContext: router.ssr!.serializer.stringify(
186
+ match.__beforeLoadContext,
187
+ ),
188
+ loaderData: router.ssr!.serializer.stringify(serializedLoaderData),
189
+ error: router.ssr!.serializer.stringify(match.error),
190
+ extracted: extracted?.map((entry) => pick(entry, ['type', 'path'])),
191
+ updatedAt: match.updatedAt,
192
+ status: match.status,
193
+ } satisfies SsrMatch,
194
+ {
195
+ isScriptContext: true,
196
+ wrap: true,
197
+ json: true,
198
+ },
199
+ )})`
200
+
201
+ router.serverSsr!.injectScript(() => initCode)
202
+
203
+ if (extracted) {
204
+ extracted.forEach((entry) => {
205
+ if (entry.type === 'promise') return injectPromise(entry)
206
+ return injectStream(entry)
207
+ })
208
+ }
209
+
210
+ function injectPromise(entry: ServerExtractedPromise) {
211
+ router.serverSsr!.injectScript(async () => {
212
+ await entry.promise
213
+
214
+ return `__TSR_SSR__.resolvePromise(${jsesc(
215
+ {
216
+ matchId: match.id,
217
+ id: entry.id,
218
+ promiseState: entry.promise[TSR_DEFERRED_PROMISE],
219
+ } satisfies ResolvePromiseState,
220
+ {
221
+ isScriptContext: true,
222
+ wrap: true,
223
+ json: true,
224
+ },
225
+ )})`
226
+ })
227
+ }
228
+
229
+ function injectStream(entry: ServerExtractedStream) {
230
+ // Inject a promise that resolves when the stream is done
231
+ // We do this to keep the stream open until we're done
232
+ router.serverSsr!.injectHtml(async () => {
233
+ //
234
+ try {
235
+ const reader = entry.stream.getReader()
236
+ let chunk: ReadableStreamReadResult<any> | null = null
237
+ while (!(chunk = await reader.read()).done) {
238
+ if (chunk.value) {
239
+ const code = `__TSR_SSR__.injectChunk(${jsesc(
240
+ {
241
+ matchId: match.id,
242
+ id: entry.id,
243
+ chunk: chunk.value,
244
+ },
245
+ {
246
+ isScriptContext: true,
247
+ wrap: true,
248
+ json: true,
249
+ },
250
+ )})`
251
+
252
+ router.serverSsr!.injectScript(() => code)
253
+ }
254
+ }
255
+
256
+ router.serverSsr!.injectScript(
257
+ () =>
258
+ `__TSR_SSR__.closeStream(${jsesc(
259
+ {
260
+ matchId: match.id,
261
+ id: entry.id,
262
+ },
263
+ {
264
+ isScriptContext: true,
265
+ wrap: true,
266
+ json: true,
267
+ },
268
+ )})`,
269
+ )
270
+ } catch (err) {
271
+ console.error('stream read error', err)
272
+ }
273
+
274
+ return ''
275
+ })
276
+ }
277
+ }
278
+
279
+ function deepImmutableSetByPath<T>(obj: T, path: Array<string>, value: any): T {
280
+ // immutable set by path retaining array and object references
281
+ if (path.length === 0) {
282
+ return value
283
+ }
284
+
285
+ const [key, ...rest] = path
286
+
287
+ if (Array.isArray(obj)) {
288
+ return obj.map((item, i) => {
289
+ if (i === Number(key)) {
290
+ return deepImmutableSetByPath(item, rest, value)
291
+ }
292
+ return item
293
+ }) as T
294
+ }
295
+
296
+ if (isPlainObject(obj)) {
297
+ return {
298
+ ...obj,
299
+ [key!]: deepImmutableSetByPath((obj as any)[key!], rest, value),
300
+ }
301
+ }
302
+
303
+ return obj
304
+ }
305
+
306
+ export function replaceBy<T>(
307
+ obj: T,
308
+ cb: (value: any, path: Array<string>) => any,
309
+ path: Array<string> = [],
310
+ ): T {
311
+ if (isPlainArray(obj)) {
312
+ return obj.map((value, i) => replaceBy(value, cb, [...path, `${i}`])) as any
313
+ }
314
+
315
+ if (isPlainObject(obj)) {
316
+ // Do not allow objects with illegal
317
+ const newObj: any = {}
318
+
319
+ for (const key in obj) {
320
+ newObj[key] = replaceBy(obj[key], cb, [...path, key])
321
+ }
322
+
323
+ return newObj
324
+ }
325
+
326
+ // // Detect classes, functions, and other non-serializable objects
327
+ // // and return undefined. Exclude some known types that are serializable
328
+ // if (
329
+ // typeof obj === 'function' ||
330
+ // (typeof obj === 'object' &&
331
+ // ![Object, Promise, ReadableStream].includes((obj as any)?.constructor))
332
+ // ) {
333
+ // console.info(obj)
334
+ // warning(false, `Non-serializable value ☝️ found at ${path.join('.')}`)
335
+ // return undefined as any
336
+ // }
337
+
338
+ const newObj = cb(obj, path)
339
+
340
+ if (newObj !== obj) {
341
+ return newObj
342
+ }
343
+
344
+ return obj
345
+ }
@@ -0,0 +1,258 @@
1
+ import { ReadableStream } from 'node:stream/web'
2
+ import { Readable } from 'node:stream'
3
+ import { createControlledPromise } from '../utils'
4
+ import type { AnyRouter } from '../router'
5
+
6
+ export function transformReadableStreamWithRouter(
7
+ router: AnyRouter,
8
+ routerStream: ReadableStream,
9
+ ) {
10
+ return transformStreamWithRouter(router, routerStream)
11
+ }
12
+
13
+ export function transformPipeableStreamWithRouter(
14
+ router: AnyRouter,
15
+ routerStream: Readable,
16
+ ) {
17
+ return Readable.fromWeb(
18
+ transformStreamWithRouter(router, Readable.toWeb(routerStream)),
19
+ )
20
+ }
21
+
22
+ // regex pattern for matching closing body and html tags
23
+ const patternBodyStart = /(<body)/
24
+ const patternBodyEnd = /(<\/body>)/
25
+ const patternHtmlEnd = /(<\/html>)/
26
+ const patternHeadStart = /(<head.*?>)/
27
+ // regex pattern for matching closing tags
28
+ const patternClosingTag = /(<\/[a-zA-Z][\w:.-]*?>)/g
29
+
30
+ const textDecoder = new TextDecoder()
31
+
32
+ type ReadablePassthrough = {
33
+ stream: ReadableStream
34
+ write: (chunk: string) => void
35
+ end: (chunk?: string) => void
36
+ destroy: (error: unknown) => void
37
+ destroyed: boolean
38
+ }
39
+
40
+ function createPassthrough() {
41
+ let controller: ReadableStreamDefaultController<any>
42
+ const encoder = new TextEncoder()
43
+ const stream = new ReadableStream({
44
+ start(c) {
45
+ controller = c
46
+ },
47
+ })
48
+
49
+ const res: ReadablePassthrough = {
50
+ stream,
51
+ write: (chunk) => {
52
+ controller.enqueue(encoder.encode(chunk))
53
+ },
54
+ end: (chunk) => {
55
+ if (chunk) {
56
+ controller.enqueue(encoder.encode(chunk))
57
+ }
58
+ controller.close()
59
+ res.destroyed = true
60
+ },
61
+ destroy: (error) => {
62
+ controller.error(error)
63
+ },
64
+ destroyed: false,
65
+ }
66
+
67
+ return res
68
+ }
69
+
70
+ async function readStream(
71
+ stream: ReadableStream,
72
+ opts: {
73
+ onData?: (chunk: ReadableStreamReadValueResult<any>) => void
74
+ onEnd?: () => void
75
+ onError?: (error: unknown) => void
76
+ },
77
+ ) {
78
+ try {
79
+ const reader = stream.getReader()
80
+ let chunk
81
+ while (!(chunk = await reader.read()).done) {
82
+ opts.onData?.(chunk)
83
+ }
84
+ opts.onEnd?.()
85
+ } catch (error) {
86
+ opts.onError?.(error)
87
+ }
88
+ }
89
+
90
+ export function transformStreamWithRouter(
91
+ router: AnyRouter,
92
+ appStream: ReadableStream,
93
+ ) {
94
+ const finalPassThrough = createPassthrough()
95
+
96
+ let isAppRendering = true as boolean
97
+ let routerStreamBuffer = ''
98
+ let pendingClosingTags = ''
99
+ let bodyStarted = false as boolean
100
+ let headStarted = false as boolean
101
+ let leftover = ''
102
+ let leftoverHtml = ''
103
+
104
+ function getBufferedRouterStream() {
105
+ const html = routerStreamBuffer
106
+ routerStreamBuffer = ''
107
+ return html
108
+ }
109
+
110
+ function decodeChunk(chunk: unknown): string {
111
+ if (chunk instanceof Uint8Array) {
112
+ return textDecoder.decode(chunk)
113
+ }
114
+ return String(chunk)
115
+ }
116
+
117
+ const injectedHtmlDonePromise = createControlledPromise<void>()
118
+
119
+ let processingCount = 0
120
+
121
+ // Process any already-injected HTML
122
+ router.serverSsr!.injectedHtml.forEach((promise) => {
123
+ handleInjectedHtml(promise)
124
+ })
125
+
126
+ // Listen for any new injected HTML
127
+ const stopListeningToInjectedHtml = router.subscribe(
128
+ 'onInjectedHtml',
129
+ (e) => {
130
+ handleInjectedHtml(e.promise)
131
+ },
132
+ )
133
+
134
+ function handleInjectedHtml(promise: Promise<string>) {
135
+ processingCount++
136
+
137
+ promise
138
+ .then((html) => {
139
+ if (!bodyStarted) {
140
+ routerStreamBuffer += html
141
+ } else {
142
+ finalPassThrough.write(html)
143
+ }
144
+ })
145
+ .catch(injectedHtmlDonePromise.reject)
146
+ .finally(() => {
147
+ processingCount--
148
+
149
+ if (!isAppRendering && processingCount === 0) {
150
+ stopListeningToInjectedHtml()
151
+ injectedHtmlDonePromise.resolve()
152
+ }
153
+ })
154
+ }
155
+
156
+ injectedHtmlDonePromise
157
+ .then(() => {
158
+ const finalHtml =
159
+ leftoverHtml + getBufferedRouterStream() + pendingClosingTags
160
+
161
+ finalPassThrough.end(finalHtml)
162
+ })
163
+ .catch((err) => {
164
+ console.error('Error reading routerStream:', err)
165
+ finalPassThrough.destroy(err)
166
+ })
167
+
168
+ // Transform the appStream
169
+ readStream(appStream, {
170
+ onData: (chunk) => {
171
+ const text = decodeChunk(chunk.value)
172
+
173
+ let chunkString = leftover + text
174
+ const bodyEndMatch = chunkString.match(patternBodyEnd)
175
+ const htmlEndMatch = chunkString.match(patternHtmlEnd)
176
+
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
196
+ }
197
+ }
198
+
199
+ if (!bodyStarted) {
200
+ finalPassThrough.write(chunkString)
201
+ leftover = ''
202
+ return
203
+ }
204
+
205
+ // If either the body end or html end is in the chunk,
206
+ // We need to get all of our data in asap
207
+ if (
208
+ bodyEndMatch &&
209
+ htmlEndMatch &&
210
+ bodyEndMatch.index! < htmlEndMatch.index!
211
+ ) {
212
+ const bodyEndIndex = bodyEndMatch.index!
213
+ pendingClosingTags = chunkString.slice(bodyEndIndex)
214
+
215
+ finalPassThrough.write(
216
+ chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),
217
+ )
218
+
219
+ leftover = ''
220
+ return
221
+ }
222
+
223
+ let result: RegExpExecArray | null
224
+ let lastIndex = 0
225
+ while ((result = patternClosingTag.exec(chunkString)) !== null) {
226
+ lastIndex = result.index + result[0].length
227
+ }
228
+
229
+ if (lastIndex > 0) {
230
+ const processed =
231
+ chunkString.slice(0, lastIndex) +
232
+ getBufferedRouterStream() +
233
+ leftoverHtml
234
+
235
+ finalPassThrough.write(processed)
236
+ leftover = chunkString.slice(lastIndex)
237
+ } else {
238
+ leftover = chunkString
239
+ leftoverHtml += getBufferedRouterStream()
240
+ }
241
+ },
242
+ onEnd: () => {
243
+ // Mark the app as done rendering
244
+ isAppRendering = false
245
+
246
+ // If there are no pending promises, resolve the injectedHtmlDonePromise
247
+ if (processingCount === 0) {
248
+ injectedHtmlDonePromise.resolve()
249
+ }
250
+ },
251
+ onError: (error) => {
252
+ console.error('Error reading appStream:', error)
253
+ finalPassThrough.destroy(error)
254
+ },
255
+ })
256
+
257
+ return finalPassThrough.stream
258
+ }
@@ -0,0 +1,91 @@
1
+ import type { ControllablePromise } from '../router'
2
+ import type { TsrSsrGlobal } from './ssr-client'
3
+
4
+ const __TSR_SSR__: TsrSsrGlobal = {
5
+ matches: [],
6
+ streamedValues: {},
7
+ initMatch: (match) => {
8
+ __TSR_SSR__.matches.push(match)
9
+
10
+ match.extracted?.forEach((ex) => {
11
+ if (ex.type === 'stream') {
12
+ let controller
13
+ ex.value = new ReadableStream({
14
+ start(c) {
15
+ controller = {
16
+ enqueue: (chunk: unknown) => {
17
+ try {
18
+ c.enqueue(chunk)
19
+ } catch {}
20
+ },
21
+ close: () => {
22
+ try {
23
+ c.close()
24
+ } catch {}
25
+ },
26
+ }
27
+ },
28
+ })
29
+ ex.value.controller = controller
30
+ } else {
31
+ let resolve: ControllablePromise['reject'] | undefined
32
+ let reject: ControllablePromise['reject'] | undefined
33
+
34
+ ex.value = new Promise((_resolve, _reject) => {
35
+ reject = _reject
36
+ resolve = _resolve
37
+ }) as ControllablePromise
38
+ ex.value.reject = reject!
39
+ ex.value.resolve = resolve!
40
+ }
41
+ })
42
+
43
+ return true
44
+ },
45
+ resolvePromise: ({ matchId, id, promiseState }) => {
46
+ const match = __TSR_SSR__.matches.find((m) => m.id === matchId)
47
+ if (match) {
48
+ const ex = match.extracted?.[id]
49
+ if (
50
+ ex &&
51
+ ex.type === 'promise' &&
52
+ ex.value &&
53
+ promiseState.status === 'success'
54
+ ) {
55
+ ex.value.resolve(promiseState.data)
56
+ return true
57
+ }
58
+ }
59
+ return false
60
+ },
61
+ injectChunk: ({ matchId, id, chunk }) => {
62
+ const match = __TSR_SSR__.matches.find((m) => m.id === matchId)
63
+
64
+ if (match) {
65
+ const ex = match.extracted?.[id]
66
+ if (ex && ex.type === 'stream' && ex.value?.controller) {
67
+ ex.value.controller.enqueue(new TextEncoder().encode(chunk.toString()))
68
+ return true
69
+ }
70
+ }
71
+ return false
72
+ },
73
+ closeStream: ({ matchId, id }) => {
74
+ const match = __TSR_SSR__.matches.find((m) => m.id === matchId)
75
+ if (match) {
76
+ const ex = match.extracted?.[id]
77
+ if (ex && ex.type === 'stream' && ex.value?.controller) {
78
+ ex.value.controller.close()
79
+ return true
80
+ }
81
+ }
82
+ return false
83
+ },
84
+ cleanScripts: () => {
85
+ document.querySelectorAll('.tsr-once').forEach((el) => {
86
+ el.remove()
87
+ })
88
+ },
89
+ }
90
+
91
+ window.__TSR_SSR__ = __TSR_SSR__
@@ -0,0 +1,4 @@
1
+ declare module '*?script-string' {
2
+ const content: string
3
+ export default content
4
+ }