@tanstack/router-core 1.171.6 → 1.171.7

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 (95) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/defer.cjs.map +1 -1
  4. package/dist/cjs/invariant.cjs.map +1 -1
  5. package/dist/cjs/load-matches.cjs.map +1 -1
  6. package/dist/cjs/lru-cache.cjs.map +1 -1
  7. package/dist/cjs/manifest.cjs.map +1 -1
  8. package/dist/cjs/new-process-route-tree.cjs.map +1 -1
  9. package/dist/cjs/not-found.cjs.map +1 -1
  10. package/dist/cjs/path.cjs.map +1 -1
  11. package/dist/cjs/qss.cjs.map +1 -1
  12. package/dist/cjs/redirect.cjs.map +1 -1
  13. package/dist/cjs/rewrite.cjs.map +1 -1
  14. package/dist/cjs/route.cjs.map +1 -1
  15. package/dist/cjs/router.cjs.map +1 -1
  16. package/dist/cjs/router.d.cts +29 -14
  17. package/dist/cjs/scroll-restoration-script/client.cjs.map +1 -1
  18. package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
  19. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  20. package/dist/cjs/searchMiddleware.cjs.map +1 -1
  21. package/dist/cjs/searchParams.cjs.map +1 -1
  22. package/dist/cjs/ssr/createRequestHandler.cjs +8 -7
  23. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  24. package/dist/cjs/ssr/handlerCallback.cjs +46 -0
  25. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -1
  26. package/dist/cjs/ssr/handlerCallback.d.cts +15 -1
  27. package/dist/cjs/ssr/headers.cjs.map +1 -1
  28. package/dist/cjs/ssr/json.cjs.map +1 -1
  29. package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
  30. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
  31. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
  32. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
  33. package/dist/cjs/ssr/server.cjs +6 -1
  34. package/dist/cjs/ssr/server.d.cts +3 -2
  35. package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
  36. package/dist/cjs/ssr/ssr-match-id.cjs.map +1 -1
  37. package/dist/cjs/ssr/ssr-server.cjs +131 -49
  38. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  39. package/dist/cjs/ssr/ssr-server.d.cts +0 -14
  40. package/dist/cjs/ssr/transformStreamWithRouter.cjs +455 -203
  41. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  42. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +14 -5
  43. package/dist/cjs/stores.cjs.map +1 -1
  44. package/dist/cjs/utils.cjs.map +1 -1
  45. package/dist/esm/Matches.js.map +1 -1
  46. package/dist/esm/config.js.map +1 -1
  47. package/dist/esm/defer.js.map +1 -1
  48. package/dist/esm/invariant.js.map +1 -1
  49. package/dist/esm/load-matches.js.map +1 -1
  50. package/dist/esm/lru-cache.js.map +1 -1
  51. package/dist/esm/manifest.js.map +1 -1
  52. package/dist/esm/new-process-route-tree.js.map +1 -1
  53. package/dist/esm/not-found.js.map +1 -1
  54. package/dist/esm/path.js.map +1 -1
  55. package/dist/esm/qss.js.map +1 -1
  56. package/dist/esm/redirect.js.map +1 -1
  57. package/dist/esm/rewrite.js.map +1 -1
  58. package/dist/esm/route.js.map +1 -1
  59. package/dist/esm/router.d.ts +29 -14
  60. package/dist/esm/router.js.map +1 -1
  61. package/dist/esm/scroll-restoration-script/client.js.map +1 -1
  62. package/dist/esm/scroll-restoration-script/server.js.map +1 -1
  63. package/dist/esm/scroll-restoration.js.map +1 -1
  64. package/dist/esm/searchMiddleware.js.map +1 -1
  65. package/dist/esm/searchParams.js.map +1 -1
  66. package/dist/esm/ssr/createRequestHandler.js +8 -7
  67. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  68. package/dist/esm/ssr/handlerCallback.d.ts +15 -1
  69. package/dist/esm/ssr/handlerCallback.js +42 -1
  70. package/dist/esm/ssr/handlerCallback.js.map +1 -1
  71. package/dist/esm/ssr/headers.js.map +1 -1
  72. package/dist/esm/ssr/json.js.map +1 -1
  73. package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
  74. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
  75. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
  76. package/dist/esm/ssr/serializer/transformer.js.map +1 -1
  77. package/dist/esm/ssr/server.d.ts +3 -2
  78. package/dist/esm/ssr/server.js +2 -2
  79. package/dist/esm/ssr/ssr-client.js.map +1 -1
  80. package/dist/esm/ssr/ssr-match-id.js.map +1 -1
  81. package/dist/esm/ssr/ssr-server.d.ts +0 -14
  82. package/dist/esm/ssr/ssr-server.js +131 -49
  83. package/dist/esm/ssr/ssr-server.js.map +1 -1
  84. package/dist/esm/ssr/transformStreamWithRouter.d.ts +14 -5
  85. package/dist/esm/ssr/transformStreamWithRouter.js +455 -203
  86. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  87. package/dist/esm/stores.js.map +1 -1
  88. package/dist/esm/utils.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/router.ts +32 -16
  91. package/src/ssr/createRequestHandler.ts +8 -8
  92. package/src/ssr/handlerCallback.ts +84 -1
  93. package/src/ssr/server.ts +14 -2
  94. package/src/ssr/ssr-server.ts +179 -81
  95. package/src/ssr/transformStreamWithRouter.ts +662 -281
@@ -16,7 +16,7 @@ import { makeSsrSerovalPlugin } from './serializer/transformer'
16
16
  import type { LRUCache } from '../lru-cache'
17
17
  import type { DehydratedMatch, DehydratedRouter } from './types'
18
18
  import type { AnySerializationAdapter } from './serializer/transformer'
19
- import type { AnyRouter } from '../router'
19
+ import type { AnyRouter, ServerSsr } from '../router'
20
20
  import type { AnyRouteMatch } from '../Matches'
21
21
  import type {
22
22
  Manifest,
@@ -26,21 +26,6 @@ import type {
26
26
  ServerManifest,
27
27
  } from '../manifest'
28
28
 
29
- declare module '../router' {
30
- interface ServerSsr {
31
- setRenderFinished: () => void
32
- cleanup: () => void
33
- }
34
- interface RouterEvents {
35
- onInjectedHtml: {
36
- type: 'onInjectedHtml'
37
- }
38
- onSerializationFinished: {
39
- type: 'onSerializationFinished'
40
- }
41
- }
42
- }
43
-
44
29
  const SCOPE_ID = 'tsr'
45
30
 
46
31
  const TSR_PREFIX = GLOBAL_TSR + '.router='
@@ -78,14 +63,15 @@ const INITIAL_SCRIPTS = [
78
63
  ]
79
64
 
80
65
  class ScriptBuffer {
81
- private router: AnyRouter | undefined
66
+ private injectScript: ((script: string) => void) | undefined
82
67
  private _queue: Array<string>
83
68
  private _scriptBarrierLifted = false
84
69
  private _cleanedUp = false
85
- private _pendingMicrotask = false
70
+ private _microtaskVersion = 0
71
+ private _pendingMicrotaskVersion = 0
86
72
 
87
- constructor(router: AnyRouter) {
88
- this.router = router
73
+ constructor(injectScript: (script: string) => void) {
74
+ this.injectScript = injectScript
89
75
  // Copy INITIAL_SCRIPTS to avoid mutating the shared array
90
76
  this._queue = INITIAL_SCRIPTS.slice()
91
77
  }
@@ -93,31 +79,39 @@ class ScriptBuffer {
93
79
  enqueue(script: string) {
94
80
  if (this._cleanedUp) return
95
81
  this._queue.push(script)
96
- // If barrier is lifted, schedule injection (if not already scheduled)
97
- if (this._scriptBarrierLifted && !this._pendingMicrotask) {
98
- this._pendingMicrotask = true
99
- queueMicrotask(() => {
100
- this._pendingMicrotask = false
101
- this.injectBufferedScripts()
102
- })
82
+ if (this._scriptBarrierLifted) {
83
+ this.scheduleInjectBufferedScripts()
103
84
  }
104
85
  }
105
86
 
106
87
  liftBarrier() {
107
88
  if (this._scriptBarrierLifted || this._cleanedUp) return
108
89
  this._scriptBarrierLifted = true
109
- if (this._queue.length > 0 && !this._pendingMicrotask) {
110
- this._pendingMicrotask = true
111
- queueMicrotask(() => {
112
- this._pendingMicrotask = false
113
- this.injectBufferedScripts()
114
- })
90
+ if (this._queue.length > 0) {
91
+ this.scheduleInjectBufferedScripts()
115
92
  }
116
93
  }
117
94
 
95
+ scheduleInjectBufferedScripts() {
96
+ if (this._pendingMicrotaskVersion !== 0) return
97
+ const pendingVersion = ++this._microtaskVersion
98
+ this._pendingMicrotaskVersion = pendingVersion
99
+ queueMicrotask(() => {
100
+ if (this._pendingMicrotaskVersion !== pendingVersion) return
101
+ this._pendingMicrotaskVersion = 0
102
+ this.injectBufferedScripts()
103
+ })
104
+ }
105
+
106
+ clearPendingMicrotask() {
107
+ if (this._pendingMicrotaskVersion === 0) return
108
+ this._pendingMicrotaskVersion = 0
109
+ this._microtaskVersion++
110
+ }
111
+
118
112
  /**
119
113
  * Flushes any pending scripts synchronously.
120
- * Call this before emitting onSerializationFinished to ensure all scripts are injected.
114
+ * Call this before signaling serialization finished to ensure all scripts are injected.
121
115
  *
122
116
  * IMPORTANT: Only injects if the barrier has been lifted. Before the barrier is lifted,
123
117
  * scripts should remain in the queue so takeBufferedScripts() can retrieve them
@@ -125,16 +119,17 @@ class ScriptBuffer {
125
119
  flush() {
126
120
  if (!this._scriptBarrierLifted) return
127
121
  if (this._cleanedUp) return
128
- this._pendingMicrotask = false
129
- const scriptsToInject = this.takeAll()
130
- if (scriptsToInject && this.router?.serverSsr) {
131
- this.router.serverSsr.injectScript(scriptsToInject)
132
- }
122
+ this.clearPendingMicrotask()
123
+ this.injectBufferedScripts()
133
124
  }
134
125
 
135
126
  takeAll() {
136
- const bufferedScripts = this._queue
137
- this._queue = []
127
+ return this.takeScripts(this._queue.length)
128
+ }
129
+
130
+ takeScripts(count: number) {
131
+ if (count <= 0) return undefined
132
+ const bufferedScripts = this._queue.splice(0, count)
138
133
  if (bufferedScripts.length === 0) {
139
134
  return undefined
140
135
  }
@@ -146,20 +141,25 @@ class ScriptBuffer {
146
141
  return bufferedScripts.join(';') + ';document.currentScript.remove()'
147
142
  }
148
143
 
144
+ hasPending() {
145
+ return this._queue.length > 0
146
+ }
147
+
149
148
  injectBufferedScripts() {
150
149
  if (this._cleanedUp) return
151
150
  // Early return if queue is empty (avoids unnecessary takeAll() call)
152
151
  if (this._queue.length === 0) return
153
152
  const scriptsToInject = this.takeAll()
154
- if (scriptsToInject && this.router?.serverSsr) {
155
- this.router.serverSsr.injectScript(scriptsToInject)
153
+ if (scriptsToInject) {
154
+ this.injectScript?.(scriptsToInject)
156
155
  }
157
156
  }
158
157
 
159
158
  cleanup() {
160
159
  this._cleanedUp = true
160
+ this.clearPendingMicrotask()
161
161
  this._queue = []
162
- this.router = undefined
162
+ this.injectScript = undefined
163
163
  }
164
164
  }
165
165
 
@@ -444,25 +444,48 @@ export function attachRouterServerSsrUtils({
444
444
  }
445
445
  let _dehydrated = false
446
446
  let _serializationFinished = false
447
+ let streamFastPathReserved = false
447
448
  const renderFinishedListeners: Array<() => void> = []
449
+ const injectedHtmlListeners: Array<() => void> = []
448
450
  const serializationFinishedListeners: Array<() => void> = []
449
- const scriptBuffer = new ScriptBuffer(router)
451
+ const cleanupListeners: Array<() => void> = []
452
+ let cleanupStarted = false
450
453
  let injectedHtmlBuffer = ''
451
454
 
452
- router.serverSsr = {
455
+ const callListeners = (listeners: Array<() => void>, errorPrefix: string) => {
456
+ const snapshot = listeners.slice()
457
+ for (const l of snapshot) {
458
+ try {
459
+ l()
460
+ } catch (err) {
461
+ console.error(`${errorPrefix}:`, err)
462
+ }
463
+ }
464
+ }
465
+
466
+ const removeListener = (
467
+ listeners: Array<() => void>,
468
+ listener: () => void,
469
+ ) => {
470
+ const index = listeners.indexOf(listener)
471
+ if (index >= 0) listeners.splice(index, 1)
472
+ }
473
+
474
+ const scriptBuffer = new ScriptBuffer((script) => {
475
+ serverSsr.injectScript(script)
476
+ })
477
+
478
+ const serverSsr: ServerSsr = {
453
479
  injectHtml: (html: string) => {
454
- if (!html) return
480
+ if (!html || cleanupStarted) return
455
481
  // Buffer the HTML so it can be retrieved via takeBufferedHtml()
456
482
  injectedHtmlBuffer += html
457
- // Emit event to notify subscribers that new HTML is available
458
- router.emit({
459
- type: 'onInjectedHtml',
460
- })
483
+ callListeners(injectedHtmlListeners, 'SSR injected HTML listener error')
461
484
  },
462
485
  injectScript: (script: string) => {
463
- if (!script) return
486
+ if (!script || cleanupStarted) return
464
487
  const html = `<script${router.options.ssr?.nonce ? ` nonce='${router.options.ssr.nonce}'` : ''}>${script}</script>`
465
- router.serverSsr!.injectHtml(html)
488
+ serverSsr.injectHtml(html)
466
489
  },
467
490
  dehydrate: async (opts?: { requestAssets?: ManifestRouteAssets }) => {
468
491
  if (_dehydrated) {
@@ -537,19 +560,34 @@ export function attachRouterServerSsrUtils({
537
560
  .concat(defaultSerovalPlugins)
538
561
  : defaultSerovalPlugins
539
562
 
563
+ let serializationCompleteSignaled = false
540
564
  const signalSerializationComplete = () => {
565
+ if (serializationCompleteSignaled || cleanupStarted) return
566
+ serializationCompleteSignaled = true
541
567
  _serializationFinished = true
542
- try {
543
- serializationFinishedListeners.forEach((l) => l())
544
- router.emit({ type: 'onSerializationFinished' })
545
- } catch (err) {
546
- console.error('Serialization listener error:', err)
547
- } finally {
548
- serializationFinishedListeners.length = 0
549
- renderFinishedListeners.length = 0
568
+
569
+ const listeners = serializationFinishedListeners.slice()
570
+ serializationFinishedListeners.length = 0
571
+
572
+ for (const l of listeners) {
573
+ try {
574
+ l()
575
+ } catch (err) {
576
+ console.error('Serialization listener error:', err)
577
+ }
550
578
  }
551
579
  }
552
580
 
581
+ const finishScriptSerialization = () => {
582
+ if (serializationCompleteSignaled || cleanupStarted) return
583
+ scriptBuffer.enqueue(GLOBAL_TSR + '.e()')
584
+ // Must synchronously notify injected HTML listeners before signaling
585
+ // completion; otherwise the held </body> tail could flush ahead of the
586
+ // end script.
587
+ scriptBuffer.flush()
588
+ signalSerializationComplete()
589
+ }
590
+
553
591
  crossSerializeStream(dehydratedRouter, {
554
592
  refs: new Map(),
555
593
  plugins,
@@ -565,15 +603,11 @@ export function attachRouterServerSsrUtils({
565
603
  if (err && (err as any).stack) {
566
604
  console.error((err as any).stack)
567
605
  }
568
- signalSerializationComplete()
606
+ finishScriptSerialization()
569
607
  },
570
608
  scopeId: SCOPE_ID,
571
609
  onDone: () => {
572
- scriptBuffer.enqueue(GLOBAL_TSR + '.e()')
573
- // Flush all pending scripts synchronously before signaling completion
574
- // This ensures all scripts are injected before onSerializationFinished is emitted
575
- scriptBuffer.flush()
576
- signalSerializationComplete()
610
+ finishScriptSerialization()
577
611
  },
578
612
  })
579
613
  },
@@ -583,23 +617,65 @@ export function attachRouterServerSsrUtils({
583
617
  isSerializationFinished() {
584
618
  return _serializationFinished
585
619
  },
586
- onRenderFinished: (listener) => renderFinishedListeners.push(listener),
587
- onSerializationFinished: (listener) =>
588
- serializationFinishedListeners.push(listener),
589
- setRenderFinished: () => {
590
- // Wrap in try-catch to ensure scriptBuffer.liftBarrier() is always called
591
- try {
592
- renderFinishedListeners.forEach((l) => l())
593
- } catch (err) {
594
- console.error('Error in render finished listener:', err)
595
- } finally {
596
- // Clear listeners after calling them to prevent memory leaks
597
- renderFinishedListeners.length = 0
620
+ reserveStreamFastPath() {
621
+ if (
622
+ !cleanupStarted &&
623
+ _serializationFinished &&
624
+ !streamFastPathReserved &&
625
+ renderFinishedListeners.length === 0 &&
626
+ !injectedHtmlBuffer &&
627
+ !scriptBuffer.hasPending()
628
+ ) {
629
+ streamFastPathReserved = true
630
+ return true
631
+ }
632
+ return false
633
+ },
634
+ onInjectedHtml: (listener) => {
635
+ if (cleanupStarted) return () => {}
636
+ injectedHtmlListeners.push(listener)
637
+ return () => removeListener(injectedHtmlListeners, listener)
638
+ },
639
+ onRenderFinished: (listener) => {
640
+ if (cleanupStarted || streamFastPathReserved) return
641
+ renderFinishedListeners.push(listener)
642
+ },
643
+ onSerializationFinished: (listener) => {
644
+ if (cleanupStarted) return () => {}
645
+ if (_serializationFinished && !cleanupStarted) {
646
+ try {
647
+ listener()
648
+ } catch (err) {
649
+ console.error('Serialization listener error:', err)
650
+ }
651
+ return () => {}
598
652
  }
653
+ serializationFinishedListeners.push(listener)
654
+ return () => removeListener(serializationFinishedListeners, listener)
655
+ },
656
+ onCleanup: (listener) => {
657
+ if (cleanupStarted) return
658
+ cleanupListeners.push(listener)
659
+ },
660
+ setRenderFinished: () => {
661
+ if (cleanupStarted) return
599
662
  scriptBuffer.liftBarrier()
663
+ const listeners = renderFinishedListeners.slice()
664
+ renderFinishedListeners.length = 0
665
+ for (const l of listeners) {
666
+ try {
667
+ l()
668
+ } catch (err) {
669
+ console.error('Error in render finished listener:', err)
670
+ }
671
+ }
672
+ if (_serializationFinished) {
673
+ scriptBuffer.flush()
674
+ }
600
675
  },
601
676
  takeBufferedScripts() {
602
677
  const scripts = scriptBuffer.takeAll()
678
+ if (!scripts) return undefined
603
679
  const serverBufferedScript: RouterManagedTag = {
604
680
  tag: 'script',
605
681
  attrs: {
@@ -623,15 +699,37 @@ export function attachRouterServerSsrUtils({
623
699
  return buffered
624
700
  },
625
701
  cleanup() {
626
- // Guard against multiple cleanup calls
627
- if (!router.serverSsr) return
702
+ // Guard against multiple/reentrant cleanup calls. A listener could call
703
+ // cleanup() again indirectly; snapshot + clear before invoking so each
704
+ // listener runs exactly once and reentry is a no-op.
705
+ if (cleanupStarted) return
706
+ cleanupStarted = true
707
+ const listeners = cleanupListeners.slice()
708
+ cleanupListeners.length = 0
709
+ for (const l of listeners) {
710
+ try {
711
+ l()
712
+ } catch (err) {
713
+ console.error('Error in SSR cleanup listener:', err)
714
+ }
715
+ }
628
716
  renderFinishedListeners.length = 0
717
+ injectedHtmlListeners.length = 0
629
718
  serializationFinishedListeners.length = 0
630
719
  injectedHtmlBuffer = ''
631
720
  scriptBuffer.cleanup()
632
721
  router.serverSsr = undefined
633
722
  },
634
723
  }
724
+
725
+ router.serverSsr = serverSsr
726
+ for (const listener of router.serverSsrLifecycle?.onServerSsrAttach ?? []) {
727
+ try {
728
+ listener(serverSsr)
729
+ } catch (err) {
730
+ console.error('SSR attach listener error:', err)
731
+ }
732
+ }
635
733
  }
636
734
 
637
735
  /**