@tanstack/router-core 1.142.7 → 1.142.8
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 +17 -5
- package/dist/cjs/ssr/constants.cjs +2 -0
- package/dist/cjs/ssr/constants.cjs.map +1 -1
- package/dist/cjs/ssr/constants.d.cts +1 -0
- package/dist/cjs/ssr/ssr-server.cjs +85 -39
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +3 -1
- package/dist/cjs/ssr/transformStreamWithRouter.cjs +202 -185
- package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
- package/dist/cjs/ssr/transformStreamWithRouter.d.cts +3 -1
- package/dist/esm/router.d.ts +17 -5
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/ssr/constants.d.ts +1 -0
- package/dist/esm/ssr/constants.js +3 -1
- package/dist/esm/ssr/constants.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +3 -1
- package/dist/esm/ssr/ssr-server.js +85 -39
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/transformStreamWithRouter.d.ts +3 -1
- package/dist/esm/ssr/transformStreamWithRouter.js +202 -185
- package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +17 -6
- package/src/ssr/constants.ts +1 -0
- package/src/ssr/ssr-server.ts +107 -46
- package/src/ssr/transformStreamWithRouter.ts +331 -253
package/src/ssr/ssr-server.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { crossSerializeStream, getCrossReferenceHeader } from 'seroval'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
|
-
import { createControlledPromise } from '../utils'
|
|
4
3
|
import minifiedTsrBootStrapScript from './tsrScript?script-string'
|
|
5
|
-
import { GLOBAL_TSR } from './constants'
|
|
4
|
+
import { GLOBAL_TSR, TSR_SCRIPT_BARRIER_ID } from './constants'
|
|
6
5
|
import { defaultSerovalPlugins } from './serializer/seroval-plugins'
|
|
7
6
|
import { makeSsrSerovalPlugin } from './serializer/transformer'
|
|
8
|
-
import { TSR_SCRIPT_BARRIER_ID } from './transformStreamWithRouter'
|
|
9
7
|
import type { DehydratedMatch, DehydratedRouter } from './types'
|
|
10
8
|
import type { AnySerializationAdapter } from './serializer/transformer'
|
|
11
9
|
import type { AnyRouter } from '../router'
|
|
@@ -20,7 +18,9 @@ declare module '../router' {
|
|
|
20
18
|
interface RouterEvents {
|
|
21
19
|
onInjectedHtml: {
|
|
22
20
|
type: 'onInjectedHtml'
|
|
23
|
-
|
|
21
|
+
}
|
|
22
|
+
onSerializationFinished: {
|
|
23
|
+
type: 'onSerializationFinished'
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -56,50 +56,76 @@ const INITIAL_SCRIPTS = [
|
|
|
56
56
|
|
|
57
57
|
class ScriptBuffer {
|
|
58
58
|
private router: AnyRouter | undefined
|
|
59
|
-
private _queue: Array<string>
|
|
59
|
+
private _queue: Array<string>
|
|
60
60
|
private _scriptBarrierLifted = false
|
|
61
61
|
private _cleanedUp = false
|
|
62
|
+
private _pendingMicrotask = false
|
|
62
63
|
|
|
63
64
|
constructor(router: AnyRouter) {
|
|
64
65
|
this.router = router
|
|
66
|
+
// Copy INITIAL_SCRIPTS to avoid mutating the shared array
|
|
67
|
+
this._queue = INITIAL_SCRIPTS.slice()
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
enqueue(script: string) {
|
|
68
71
|
if (this._cleanedUp) return
|
|
69
|
-
|
|
72
|
+
this._queue.push(script)
|
|
73
|
+
// If barrier is lifted, schedule injection (if not already scheduled)
|
|
74
|
+
if (this._scriptBarrierLifted && !this._pendingMicrotask) {
|
|
75
|
+
this._pendingMicrotask = true
|
|
70
76
|
queueMicrotask(() => {
|
|
77
|
+
this._pendingMicrotask = false
|
|
71
78
|
this.injectBufferedScripts()
|
|
72
79
|
})
|
|
73
80
|
}
|
|
74
|
-
this._queue.push(script)
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
liftBarrier() {
|
|
78
84
|
if (this._scriptBarrierLifted || this._cleanedUp) return
|
|
79
85
|
this._scriptBarrierLifted = true
|
|
80
|
-
if (this._queue.length > 0) {
|
|
86
|
+
if (this._queue.length > 0 && !this._pendingMicrotask) {
|
|
87
|
+
this._pendingMicrotask = true
|
|
81
88
|
queueMicrotask(() => {
|
|
89
|
+
this._pendingMicrotask = false
|
|
82
90
|
this.injectBufferedScripts()
|
|
83
91
|
})
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Flushes any pending scripts synchronously.
|
|
97
|
+
* Call this before emitting onSerializationFinished to ensure all scripts are injected.
|
|
98
|
+
*
|
|
99
|
+
* IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,
|
|
100
|
+
* scripts should remain in the queue so takeBufferedScripts() can retrieve them
|
|
101
|
+
*/
|
|
102
|
+
flush() {
|
|
103
|
+
if (!this._scriptBarrierLifted) return
|
|
104
|
+
if (this._cleanedUp) return
|
|
105
|
+
this._pendingMicrotask = false
|
|
106
|
+
const scriptsToInject = this.takeAll()
|
|
107
|
+
if (scriptsToInject && this.router?.serverSsr) {
|
|
108
|
+
this.router.serverSsr.injectScript(scriptsToInject)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
takeAll() {
|
|
88
113
|
const bufferedScripts = this._queue
|
|
89
114
|
this._queue = []
|
|
90
115
|
if (bufferedScripts.length === 0) {
|
|
91
116
|
return undefined
|
|
92
117
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return joinedScripts
|
|
118
|
+
// Append cleanup script and join - avoid push() to not mutate then iterate
|
|
119
|
+
return bufferedScripts.join(';') + ';document.currentScript.remove()'
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
injectBufferedScripts() {
|
|
99
123
|
if (this._cleanedUp) return
|
|
124
|
+
// Early return if queue is empty (avoids unnecessary takeAll() call)
|
|
125
|
+
if (this._queue.length === 0) return
|
|
100
126
|
const scriptsToInject = this.takeAll()
|
|
101
127
|
if (scriptsToInject && this.router?.serverSsr) {
|
|
102
|
-
this.router.serverSsr.injectScript(
|
|
128
|
+
this.router.serverSsr.injectScript(scriptsToInject)
|
|
103
129
|
}
|
|
104
130
|
}
|
|
105
131
|
|
|
@@ -121,29 +147,26 @@ export function attachRouterServerSsrUtils({
|
|
|
121
147
|
manifest,
|
|
122
148
|
}
|
|
123
149
|
let _dehydrated = false
|
|
124
|
-
|
|
150
|
+
let _serializationFinished = false
|
|
151
|
+
const renderFinishedListeners: Array<() => void> = []
|
|
152
|
+
const serializationFinishedListeners: Array<() => void> = []
|
|
125
153
|
const scriptBuffer = new ScriptBuffer(router)
|
|
154
|
+
let injectedHtmlBuffer: Array<string> = []
|
|
126
155
|
|
|
127
156
|
router.serverSsr = {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
157
|
+
injectHtml: (html: string) => {
|
|
158
|
+
if (!html) return
|
|
159
|
+
// Buffer the HTML so it can be retrieved via takeBufferedHtml()
|
|
160
|
+
injectedHtmlBuffer.push(html)
|
|
161
|
+
// Emit event to notify subscribers that new HTML is available
|
|
132
162
|
router.emit({
|
|
133
163
|
type: 'onInjectedHtml',
|
|
134
|
-
promise,
|
|
135
164
|
})
|
|
136
|
-
|
|
137
|
-
return promise.then(() => {})
|
|
138
165
|
},
|
|
139
|
-
injectScript: (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return ''
|
|
144
|
-
}
|
|
145
|
-
return `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`
|
|
146
|
-
})
|
|
166
|
+
injectScript: (script: string) => {
|
|
167
|
+
if (!script) return
|
|
168
|
+
const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`
|
|
169
|
+
router.serverSsr!.injectHtml(html)
|
|
147
170
|
},
|
|
148
171
|
dehydrate: async () => {
|
|
149
172
|
invariant(!_dehydrated, 'router is already dehydrated!')
|
|
@@ -201,18 +224,32 @@ export function attachRouterServerSsrUtils({
|
|
|
201
224
|
}
|
|
202
225
|
_dehydrated = true
|
|
203
226
|
|
|
204
|
-
const p = createControlledPromise<string>()
|
|
205
227
|
const trackPlugins = { didRun: false }
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
const serializationAdapters = router.options.serializationAdapters as
|
|
229
|
+
| Array<AnySerializationAdapter>
|
|
230
|
+
| undefined
|
|
231
|
+
const plugins = serializationAdapters
|
|
232
|
+
? serializationAdapters
|
|
233
|
+
.map((t) => makeSsrSerovalPlugin(t, trackPlugins))
|
|
234
|
+
.concat(defaultSerovalPlugins)
|
|
235
|
+
: defaultSerovalPlugins
|
|
236
|
+
|
|
237
|
+
const signalSerializationComplete = () => {
|
|
238
|
+
_serializationFinished = true
|
|
239
|
+
try {
|
|
240
|
+
serializationFinishedListeners.forEach((l) => l())
|
|
241
|
+
router.emit({ type: 'onSerializationFinished' })
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error('Serialization listener error:', err)
|
|
244
|
+
} finally {
|
|
245
|
+
serializationFinishedListeners.length = 0
|
|
246
|
+
renderFinishedListeners.length = 0
|
|
247
|
+
}
|
|
248
|
+
}
|
|
212
249
|
|
|
213
250
|
crossSerializeStream(dehydratedRouter, {
|
|
214
251
|
refs: new Map(),
|
|
215
|
-
plugins
|
|
252
|
+
plugins,
|
|
216
253
|
onSerialize: (data, initial) => {
|
|
217
254
|
let serialized = initial ? GLOBAL_TSR + '.router=' + data : data
|
|
218
255
|
if (trackPlugins.didRun) {
|
|
@@ -223,21 +260,36 @@ export function attachRouterServerSsrUtils({
|
|
|
223
260
|
scopeId: SCOPE_ID,
|
|
224
261
|
onDone: () => {
|
|
225
262
|
scriptBuffer.enqueue(GLOBAL_TSR + '.e()')
|
|
226
|
-
|
|
263
|
+
// Flush all pending scripts synchronously before signaling completion
|
|
264
|
+
// This ensures all scripts are injected before onSerializationFinished is emitted
|
|
265
|
+
scriptBuffer.flush()
|
|
266
|
+
signalSerializationComplete()
|
|
267
|
+
},
|
|
268
|
+
onError: (err) => {
|
|
269
|
+
console.error('Serialization error:', err)
|
|
270
|
+
signalSerializationComplete()
|
|
227
271
|
},
|
|
228
|
-
onError: (err) => p.reject(err),
|
|
229
272
|
})
|
|
230
|
-
// make sure the stream is kept open until the promise is resolved
|
|
231
|
-
router.serverSsr!.injectHtml(() => p)
|
|
232
273
|
},
|
|
233
274
|
isDehydrated() {
|
|
234
275
|
return _dehydrated
|
|
235
276
|
},
|
|
236
|
-
|
|
277
|
+
isSerializationFinished() {
|
|
278
|
+
return _serializationFinished
|
|
279
|
+
},
|
|
280
|
+
onRenderFinished: (listener) => renderFinishedListeners.push(listener),
|
|
281
|
+
onSerializationFinished: (listener) =>
|
|
282
|
+
serializationFinishedListeners.push(listener),
|
|
237
283
|
setRenderFinished: () => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
284
|
+
// Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called
|
|
285
|
+
try {
|
|
286
|
+
renderFinishedListeners.forEach((l) => l())
|
|
287
|
+
} catch (err) {
|
|
288
|
+
console.error('Error in render finished listener:', err)
|
|
289
|
+
} finally {
|
|
290
|
+
// Clear listeners after calling them to prevent memory leaks
|
|
291
|
+
renderFinishedListeners.length = 0
|
|
292
|
+
}
|
|
241
293
|
scriptBuffer.liftBarrier()
|
|
242
294
|
},
|
|
243
295
|
takeBufferedScripts() {
|
|
@@ -256,12 +308,21 @@ export function attachRouterServerSsrUtils({
|
|
|
256
308
|
liftScriptBarrier() {
|
|
257
309
|
scriptBuffer.liftBarrier()
|
|
258
310
|
},
|
|
311
|
+
takeBufferedHtml() {
|
|
312
|
+
if (injectedHtmlBuffer.length === 0) {
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
const buffered = injectedHtmlBuffer.join('')
|
|
316
|
+
injectedHtmlBuffer = []
|
|
317
|
+
return buffered
|
|
318
|
+
},
|
|
259
319
|
cleanup() {
|
|
260
320
|
// Guard against multiple cleanup calls
|
|
261
321
|
if (!router.serverSsr) return
|
|
262
|
-
|
|
322
|
+
renderFinishedListeners.length = 0
|
|
323
|
+
serializationFinishedListeners.length = 0
|
|
324
|
+
injectedHtmlBuffer = []
|
|
263
325
|
scriptBuffer.cleanup()
|
|
264
|
-
router.serverSsr.injectedHtml = []
|
|
265
326
|
router.serverSsr = undefined
|
|
266
327
|
},
|
|
267
328
|
}
|