@tanstack/vue-router 1.140.5 → 1.141.1

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 (150) hide show
  1. package/dist/esm/Asset.js +122 -8
  2. package/dist/esm/Asset.js.map +1 -1
  3. package/dist/esm/Body.d.ts +4 -0
  4. package/dist/esm/Body.js +26 -0
  5. package/dist/esm/Body.js.map +1 -0
  6. package/dist/esm/CatchBoundary.d.ts +1 -1
  7. package/dist/esm/CatchBoundary.js +8 -8
  8. package/dist/esm/CatchBoundary.js.map +1 -1
  9. package/dist/esm/Html.d.ts +4 -0
  10. package/dist/esm/Html.js +63 -0
  11. package/dist/esm/Html.js.map +1 -0
  12. package/dist/esm/Match.js +87 -49
  13. package/dist/esm/Match.js.map +1 -1
  14. package/dist/esm/Matches.js +3 -2
  15. package/dist/esm/Matches.js.map +1 -1
  16. package/dist/esm/RouterProvider.js +3 -0
  17. package/dist/esm/RouterProvider.js.map +1 -1
  18. package/dist/esm/ScriptOnce.d.ts +12 -5
  19. package/dist/esm/ScriptOnce.js +35 -15
  20. package/dist/esm/ScriptOnce.js.map +1 -1
  21. package/dist/esm/Scripts.d.ts +2 -1
  22. package/dist/esm/Scripts.js +101 -35
  23. package/dist/esm/Scripts.js.map +1 -1
  24. package/dist/esm/Transitioner.d.ts +16 -0
  25. package/dist/esm/Transitioner.js +136 -133
  26. package/dist/esm/Transitioner.js.map +1 -1
  27. package/dist/esm/awaited.d.ts +20 -5
  28. package/dist/esm/awaited.js +17 -20
  29. package/dist/esm/awaited.js.map +1 -1
  30. package/dist/esm/index.d.ts +2 -0
  31. package/dist/esm/index.js +4 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/lazyRouteComponent.js +2 -2
  34. package/dist/esm/lazyRouteComponent.js.map +1 -1
  35. package/dist/esm/link.js +27 -35
  36. package/dist/esm/link.js.map +1 -1
  37. package/dist/esm/scroll-restoration.d.ts +8 -1
  38. package/dist/esm/scroll-restoration.js +44 -12
  39. package/dist/esm/scroll-restoration.js.map +1 -1
  40. package/dist/esm/ssr/RouterClient.d.ts +15 -0
  41. package/dist/esm/ssr/RouterClient.js +46 -0
  42. package/dist/esm/ssr/RouterClient.js.map +1 -0
  43. package/dist/esm/ssr/RouterServer.d.ts +15 -0
  44. package/dist/esm/ssr/RouterServer.js +37 -0
  45. package/dist/esm/ssr/RouterServer.js.map +1 -0
  46. package/dist/esm/ssr/client.d.ts +1 -0
  47. package/dist/esm/ssr/client.js +5 -0
  48. package/dist/esm/ssr/client.js.map +1 -0
  49. package/dist/esm/ssr/defaultRenderHandler.d.ts +1 -0
  50. package/dist/esm/ssr/defaultRenderHandler.js +15 -0
  51. package/dist/esm/ssr/defaultRenderHandler.js.map +1 -0
  52. package/dist/esm/ssr/defaultStreamHandler.d.ts +1 -0
  53. package/dist/esm/ssr/defaultStreamHandler.js +17 -0
  54. package/dist/esm/ssr/defaultStreamHandler.js.map +1 -0
  55. package/dist/esm/ssr/renderRouterToStream.d.ts +8 -0
  56. package/dist/esm/ssr/renderRouterToStream.js +70 -0
  57. package/dist/esm/ssr/renderRouterToStream.js.map +1 -0
  58. package/dist/esm/ssr/renderRouterToString.d.ts +7 -0
  59. package/dist/esm/ssr/renderRouterToString.js +33 -0
  60. package/dist/esm/ssr/renderRouterToString.js.map +1 -0
  61. package/dist/esm/ssr/server.d.ts +6 -0
  62. package/dist/esm/ssr/server.js +14 -0
  63. package/dist/esm/ssr/server.js.map +1 -0
  64. package/dist/source/Asset.jsx +119 -7
  65. package/dist/source/Asset.jsx.map +1 -1
  66. package/dist/source/Body.d.ts +4 -0
  67. package/dist/source/Body.jsx +15 -0
  68. package/dist/source/Body.jsx.map +1 -0
  69. package/dist/source/CatchBoundary.d.ts +1 -1
  70. package/dist/source/CatchBoundary.jsx +10 -23
  71. package/dist/source/CatchBoundary.jsx.map +1 -1
  72. package/dist/source/Html.d.ts +4 -0
  73. package/dist/source/Html.jsx +56 -0
  74. package/dist/source/Html.jsx.map +1 -0
  75. package/dist/source/Match.jsx +119 -54
  76. package/dist/source/Match.jsx.map +1 -1
  77. package/dist/source/Matches.jsx +15 -3
  78. package/dist/source/Matches.jsx.map +1 -1
  79. package/dist/source/RouterProvider.jsx +5 -0
  80. package/dist/source/RouterProvider.jsx.map +1 -1
  81. package/dist/source/ScriptOnce.d.ts +12 -5
  82. package/dist/source/ScriptOnce.jsx +27 -16
  83. package/dist/source/ScriptOnce.jsx.map +1 -1
  84. package/dist/source/Scripts.d.ts +2 -1
  85. package/dist/source/Scripts.jsx +100 -42
  86. package/dist/source/Scripts.jsx.map +1 -1
  87. package/dist/source/Transitioner.d.ts +16 -0
  88. package/dist/source/Transitioner.jsx +180 -160
  89. package/dist/source/Transitioner.jsx.map +1 -1
  90. package/dist/source/awaited.d.ts +20 -5
  91. package/dist/source/awaited.jsx +18 -25
  92. package/dist/source/awaited.jsx.map +1 -1
  93. package/dist/source/index.d.ts +2 -0
  94. package/dist/source/index.jsx +2 -0
  95. package/dist/source/index.jsx.map +1 -1
  96. package/dist/source/lazyRouteComponent.jsx +4 -2
  97. package/dist/source/lazyRouteComponent.jsx.map +1 -1
  98. package/dist/source/link.jsx +37 -51
  99. package/dist/source/link.jsx.map +1 -1
  100. package/dist/source/scroll-restoration.d.ts +8 -1
  101. package/dist/source/scroll-restoration.jsx +55 -12
  102. package/dist/source/scroll-restoration.jsx.map +1 -1
  103. package/dist/source/ssr/RouterClient.d.ts +15 -0
  104. package/dist/source/ssr/RouterClient.jsx +48 -0
  105. package/dist/source/ssr/RouterClient.jsx.map +1 -0
  106. package/dist/source/ssr/RouterServer.d.ts +15 -0
  107. package/dist/source/ssr/RouterServer.jsx +40 -0
  108. package/dist/source/ssr/RouterServer.jsx.map +1 -0
  109. package/dist/source/ssr/client.d.ts +1 -0
  110. package/dist/source/ssr/client.js +2 -0
  111. package/dist/source/ssr/client.js.map +1 -0
  112. package/dist/source/ssr/defaultRenderHandler.d.ts +1 -0
  113. package/dist/source/ssr/defaultRenderHandler.jsx +9 -0
  114. package/dist/source/ssr/defaultRenderHandler.jsx.map +1 -0
  115. package/dist/source/ssr/defaultStreamHandler.d.ts +1 -0
  116. package/dist/source/ssr/defaultStreamHandler.jsx +10 -0
  117. package/dist/source/ssr/defaultStreamHandler.jsx.map +1 -0
  118. package/dist/source/ssr/renderRouterToStream.d.ts +8 -0
  119. package/dist/source/ssr/renderRouterToStream.jsx +55 -0
  120. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -0
  121. package/dist/source/ssr/renderRouterToString.d.ts +7 -0
  122. package/dist/source/ssr/renderRouterToString.jsx +26 -0
  123. package/dist/source/ssr/renderRouterToString.jsx.map +1 -0
  124. package/dist/source/ssr/server.d.ts +6 -0
  125. package/dist/source/ssr/server.js +7 -0
  126. package/dist/source/ssr/server.js.map +1 -0
  127. package/package.json +16 -3
  128. package/src/Asset.tsx +157 -7
  129. package/src/Body.tsx +26 -0
  130. package/src/CatchBoundary.tsx +11 -25
  131. package/src/Html.tsx +65 -0
  132. package/src/Match.tsx +135 -58
  133. package/src/Matches.tsx +16 -4
  134. package/src/RouterProvider.tsx +6 -0
  135. package/src/ScriptOnce.tsx +43 -28
  136. package/src/Scripts.tsx +121 -56
  137. package/src/Transitioner.tsx +197 -176
  138. package/src/awaited.tsx +17 -28
  139. package/src/index.tsx +2 -0
  140. package/src/lazyRouteComponent.tsx +4 -2
  141. package/src/link.tsx +42 -47
  142. package/src/scroll-restoration.tsx +69 -21
  143. package/src/ssr/RouterClient.tsx +58 -0
  144. package/src/ssr/RouterServer.tsx +51 -0
  145. package/src/ssr/client.ts +1 -0
  146. package/src/ssr/defaultRenderHandler.tsx +12 -0
  147. package/src/ssr/defaultStreamHandler.tsx +13 -0
  148. package/src/ssr/renderRouterToStream.tsx +85 -0
  149. package/src/ssr/renderRouterToString.tsx +37 -0
  150. package/src/ssr/server.ts +6 -0
package/src/Match.tsx CHANGED
@@ -28,18 +28,57 @@ export const Match = Vue.defineComponent({
28
28
  },
29
29
  setup(props) {
30
30
  const router = useRouter()
31
- const routeId = useRouterState({
31
+
32
+ // Track the last known routeId to handle stale props during same-route transitions
33
+ let lastKnownRouteId: string | null = null
34
+
35
+ // Combined selector that returns all needed data including the actual matchId
36
+ // This handles stale props.matchId during same-route transitions
37
+ const matchData = useRouterState({
32
38
  select: (s) => {
33
- return s.matches.find((d) => d.id === props.matchId)?.routeId as string
39
+ // First try to find match by props.matchId
40
+ let match = s.matches.find((d) => d.id === props.matchId)
41
+ let matchIndex = match
42
+ ? s.matches.findIndex((d) => d.id === props.matchId)
43
+ : -1
44
+
45
+ // If match found, update lastKnownRouteId
46
+ if (match) {
47
+ lastKnownRouteId = match.routeId as string
48
+ } else if (lastKnownRouteId) {
49
+ // Match not found - props.matchId might be stale during a same-route transition
50
+ // Try to find the NEW match by routeId
51
+ match = s.matches.find((d) => d.routeId === lastKnownRouteId)
52
+ matchIndex = match
53
+ ? s.matches.findIndex((d) => d.routeId === lastKnownRouteId)
54
+ : -1
55
+ }
56
+
57
+ if (!match) {
58
+ return null
59
+ }
60
+
61
+ const routeId = match.routeId as string
62
+ const parentRouteId =
63
+ matchIndex > 0 ? (s.matches[matchIndex - 1]?.routeId as string) : null
64
+
65
+ return {
66
+ matchId: match.id, // Return the actual matchId (may differ from props.matchId)
67
+ routeId,
68
+ parentRouteId,
69
+ loadedAt: s.loadedAt,
70
+ }
34
71
  },
35
72
  })
36
73
 
37
74
  invariant(
38
- routeId.value,
75
+ matchData.value,
39
76
  `Could not find routeId for matchId "${props.matchId}". Please file an issue!`,
40
77
  )
41
78
 
42
- const route = Vue.computed(() => router.routesById[routeId.value])
79
+ const route = Vue.computed(() =>
80
+ matchData.value ? router.routesById[matchData.value.routeId] : null,
81
+ )
43
82
 
44
83
  const PendingComponent = Vue.computed(
45
84
  () =>
@@ -65,25 +104,18 @@ export const Match = Vue.defineComponent({
65
104
  : route.value?.options?.notFoundComponent,
66
105
  )
67
106
 
68
- const resetKey = useRouterState({
69
- select: (s) => s.loadedAt,
70
- })
107
+ // Create a ref for the current matchId that we provide to child components
108
+ // This ref is updated to the ACTUAL matchId found (which may differ from props during transitions)
109
+ const matchIdRef = Vue.ref(matchData.value?.matchId ?? props.matchId)
71
110
 
72
- const parentRouteId = useRouterState({
73
- select: (s) => {
74
- const index = s.matches.findIndex((d) => d.id === props.matchId)
75
- return s.matches[index - 1]?.routeId as string
76
- },
77
- })
78
-
79
- // Create a ref for the current matchId that we can provide to child components
80
- const matchIdRef = Vue.ref(props.matchId)
81
-
82
- // When props.matchId changes, update the ref
111
+ // Watch both props.matchId and matchData to keep matchIdRef in sync
112
+ // This ensures Outlet gets the correct matchId even during transitions
83
113
  Vue.watch(
84
- () => props.matchId,
85
- (newMatchId) => {
86
- matchIdRef.value = newMatchId
114
+ [() => props.matchId, () => matchData.value?.matchId],
115
+ ([propsMatchId, dataMatchId]) => {
116
+ // Prefer the matchId from matchData (which handles fallback)
117
+ // Fall back to props.matchId if matchData is null
118
+ matchIdRef.value = dataMatchId ?? propsMatchId
87
119
  },
88
120
  { immediate: true },
89
121
  )
@@ -92,8 +124,11 @@ export const Match = Vue.defineComponent({
92
124
  Vue.provide(matchContext, matchIdRef)
93
125
 
94
126
  return (): VNode => {
127
+ // Use the actual matchId from matchData, not props (which may be stale)
128
+ const actualMatchId = matchData.value?.matchId ?? props.matchId
129
+
95
130
  // Determine which components to render
96
- let content: VNode = Vue.h(MatchInner, { matchId: props.matchId })
131
+ let content: VNode = Vue.h(MatchInner, { matchId: actualMatchId })
97
132
 
98
133
  // Wrap in NotFound boundary if needed
99
134
  if (routeNotFoundComponent.value) {
@@ -103,7 +138,7 @@ export const Match = Vue.defineComponent({
103
138
  // route ID which doesn't match the current route, rethrow the error
104
139
  if (
105
140
  !routeNotFoundComponent.value ||
106
- (error.routeId && error.routeId !== routeId.value) ||
141
+ (error.routeId && error.routeId !== matchData.value?.routeId) ||
107
142
  (!error.routeId && route.value && !route.value.isRoot)
108
143
  )
109
144
  throw error
@@ -117,12 +152,12 @@ export const Match = Vue.defineComponent({
117
152
  // Wrap in error boundary if needed
118
153
  if (routeErrorComponent.value) {
119
154
  content = CatchBoundary({
120
- getResetKey: () => resetKey.value,
155
+ getResetKey: () => matchData.value?.loadedAt ?? 0,
121
156
  errorComponent: routeErrorComponent.value || ErrorComponent,
122
157
  onCatch: (error: Error) => {
123
158
  // Forward not found errors (we don't want to show the error component for these)
124
159
  if (isNotFound(error)) throw error
125
- warning(false, `Error in route match: ${props.matchId}`)
160
+ warning(false, `Error in route match: ${actualMatchId}`)
126
161
  routeOnCatch.value?.(error)
127
162
  },
128
163
  children: content,
@@ -154,7 +189,8 @@ export const Match = Vue.defineComponent({
154
189
  // Add scroll restoration if needed
155
190
  const withScrollRestoration: Array<VNode> = [
156
191
  content,
157
- parentRouteId.value === rootRouteId && router.options.scrollRestoration
192
+ matchData.value?.parentRouteId === rootRouteId &&
193
+ router.options.scrollRestoration
158
194
  ? Vue.h(Vue.Fragment, null, [
159
195
  Vue.h(OnRendered),
160
196
  Vue.h(ScrollRestoration),
@@ -162,6 +198,10 @@ export const Match = Vue.defineComponent({
162
198
  : null,
163
199
  ].filter(Boolean) as Array<VNode>
164
200
 
201
+ // Return single child directly to avoid Fragment wrapper that causes hydration mismatch
202
+ if (withScrollRestoration.length === 1) {
203
+ return withScrollRestoration[0]!
204
+ }
165
205
  return Vue.h(Vue.Fragment, null, withScrollRestoration)
166
206
  }
167
207
  },
@@ -209,61 +249,78 @@ export const MatchInner = Vue.defineComponent({
209
249
  setup(props) {
210
250
  const router = useRouter()
211
251
 
212
- // { match, key, routeId } =
213
- const matchState = useRouterState({
252
+ // Track the last known routeId to handle stale props during same-route transitions
253
+ // This is stored outside the selector so it persists across selector calls
254
+ let lastKnownRouteId: string | null = null
255
+
256
+ // Combined selector for match state AND remount key
257
+ // This ensures both are computed in the same selector call with consistent data
258
+ const combinedState = useRouterState({
214
259
  select: (s) => {
215
- const match = s.matches.find((d) => d.id === props.matchId)
260
+ // First try to find match by props.matchId
261
+ let match = s.matches.find((d) => d.id === props.matchId)
262
+
263
+ // If match found, update lastKnownRouteId
264
+ if (match) {
265
+ lastKnownRouteId = match.routeId as string
266
+ } else if (lastKnownRouteId) {
267
+ // Match not found - props.matchId might be stale during a same-route transition
268
+ // (matchId changed due to loaderDepsHash but props haven't updated yet)
269
+ // Try to find the NEW match by routeId and use that instead
270
+ const sameRouteMatch = s.matches.find(
271
+ (d) => d.routeId === lastKnownRouteId,
272
+ )
273
+ if (sameRouteMatch) {
274
+ match = sameRouteMatch
275
+ }
276
+ }
216
277
 
217
- // During navigation transitions, matches can be temporarily removed
218
278
  if (!match) {
279
+ // Route no longer exists - truly navigating away
219
280
  return null
220
281
  }
221
282
 
222
283
  const routeId = match.routeId as string
223
284
 
285
+ // Compute remount key
224
286
  const remountFn =
225
287
  (router.routesById[routeId] as AnyRoute).options.remountDeps ??
226
288
  router.options.defaultRemountDeps
227
- const remountDeps = remountFn?.({
228
- routeId,
229
- loaderDeps: match.loaderDeps,
230
- params: match._strictParams,
231
- search: match._strictSearch,
232
- })
233
- const key = remountDeps ? JSON.stringify(remountDeps) : undefined
289
+
290
+ let remountKey: string | undefined
291
+ if (remountFn) {
292
+ const remountDeps = remountFn({
293
+ routeId,
294
+ loaderDeps: match.loaderDeps,
295
+ params: match._strictParams,
296
+ search: match._strictSearch,
297
+ })
298
+ remountKey = remountDeps ? JSON.stringify(remountDeps) : undefined
299
+ }
234
300
 
235
301
  return {
236
- key,
237
302
  routeId,
238
303
  match: {
239
304
  id: match.id,
240
305
  status: match.status,
241
306
  error: match.error,
242
307
  },
308
+ remountKey,
243
309
  }
244
310
  },
245
311
  })
246
312
 
247
313
  const route = Vue.computed(() => {
248
- if (!matchState.value) return null
249
- return router.routesById[matchState.value.routeId]!
314
+ if (!combinedState.value) return null
315
+ return router.routesById[combinedState.value.routeId]!
250
316
  })
251
317
 
252
- const match = Vue.computed(() => matchState.value?.match)
253
-
254
- const out = Vue.computed((): VNode | null => {
255
- if (!route.value) return null
256
- const Comp =
257
- route.value.options.component ?? router.options.defaultComponent
258
- if (Comp) {
259
- return Vue.h(Comp)
260
- }
261
- return Vue.h(Outlet)
262
- })
318
+ const match = Vue.computed(() => combinedState.value?.match)
319
+ const remountKey = Vue.computed(() => combinedState.value?.remountKey)
263
320
 
264
321
  return (): VNode | null => {
265
322
  // If match doesn't exist, return null (component is being unmounted or not ready)
266
- if (!matchState.value || !match.value || !route.value) {
323
+ if (!combinedState.value || !match.value || !route.value) {
267
324
  return null
268
325
  }
269
326
 
@@ -341,8 +398,17 @@ export const MatchInner = Vue.defineComponent({
341
398
  return null
342
399
  }
343
400
 
344
- // Success status - render the component
345
- return out.value
401
+ // Success status - render the component with remount key
402
+ const Comp =
403
+ route.value.options.component ?? router.options.defaultComponent
404
+ const key = remountKey.value
405
+
406
+ if (Comp) {
407
+ // Pass key as a prop - Vue.h properly handles 'key' as a special prop
408
+ return Vue.h(Comp, key !== undefined ? { key } : undefined)
409
+ }
410
+
411
+ return Vue.h(Outlet, key !== undefined ? { key } : undefined)
346
412
  }
347
413
  },
348
414
  })
@@ -376,11 +442,19 @@ export const Outlet = Vue.defineComponent({
376
442
  },
377
443
  })
378
444
 
379
- const childMatchId = useRouterState({
445
+ const childMatchData = useRouterState({
380
446
  select: (s) => {
381
447
  const matches = s.matches
382
448
  const index = matches.findIndex((d) => d.id === safeMatchId.value)
383
- return matches[index + 1]?.id
449
+ const child = matches[index + 1]
450
+ if (!child) return null
451
+ return {
452
+ id: child.id,
453
+ // Key based on routeId + params only (not loaderDeps)
454
+ // This ensures component recreates when params change,
455
+ // but NOT when only loaderDeps change
456
+ paramsKey: child.routeId + JSON.stringify(child._strictParams),
457
+ }
384
458
  },
385
459
  })
386
460
 
@@ -389,11 +463,14 @@ export const Outlet = Vue.defineComponent({
389
463
  return renderRouteNotFound(router, route.value, undefined)
390
464
  }
391
465
 
392
- if (!childMatchId.value) {
466
+ if (!childMatchData.value) {
393
467
  return null
394
468
  }
395
469
 
396
- const nextMatch = Vue.h(Match, { matchId: childMatchId.value })
470
+ const nextMatch = Vue.h(Match, {
471
+ matchId: childMatchData.value.id,
472
+ key: childMatchData.value.paramsKey,
473
+ })
397
474
 
398
475
  if (safeMatchId.value === rootRouteId) {
399
476
  return Vue.h(
package/src/Matches.tsx CHANGED
@@ -3,7 +3,7 @@ import warning from 'tiny-warning'
3
3
  import { CatchBoundary } from './CatchBoundary'
4
4
  import { useRouterState } from './useRouterState'
5
5
  import { useRouter } from './useRouter'
6
- import { Transitioner } from './Transitioner'
6
+ import { useTransitionerSetup } from './Transitioner'
7
7
  import { matchContext } from './matchContext'
8
8
  import { Match } from './Match'
9
9
  import type {
@@ -36,12 +36,24 @@ declare module '@tanstack/router-core' {
36
36
  }
37
37
  }
38
38
 
39
- // Create a component that renders both the Transitioner and MatchesInner
39
+ // Create a component that renders MatchesInner with Transitioner's setup logic inlined.
40
+ // This is critical for proper hydration - we call useTransitionerSetup() as a composable
41
+ // rather than rendering it as a component, which avoids Fragment/element mismatches.
40
42
  const MatchesContent = Vue.defineComponent({
41
43
  name: 'MatchesContent',
42
44
  setup() {
43
- return () =>
44
- Vue.h(Vue.Fragment, null, [Vue.h(Transitioner), Vue.h(MatchesInner)])
45
+ // IMPORTANT: We need to ensure Transitioner's setup() runs.
46
+ // Transitioner sets up critical functionality:
47
+ // - router.startTransition
48
+ // - History subscription via router.history.subscribe(router.load)
49
+ // - Watchers for router events
50
+ //
51
+ // We inline Transitioner's setup logic here. Since Transitioner returns null,
52
+ // we can call its setup function directly without affecting the render tree.
53
+ // This is done by importing and calling useTransitionerSetup.
54
+ useTransitionerSetup()
55
+
56
+ return () => Vue.h(MatchesInner)
45
57
  },
46
58
  })
47
59
 
@@ -44,6 +44,12 @@ export const RouterContextProvider = Vue.defineComponent({
44
44
  return Vue.h(WrapComponent, null, () => childContent)
45
45
  }
46
46
 
47
+ // Unwrap single-element arrays to avoid implicit Fragment
48
+ // that would cause hydration mismatch
49
+ if (Array.isArray(childContent) && childContent.length === 1) {
50
+ return childContent[0]
51
+ }
52
+
47
53
  // Otherwise just return the child content
48
54
  return childContent
49
55
  }
@@ -1,30 +1,45 @@
1
- import jsesc from 'jsesc'
1
+ import * as Vue from 'vue'
2
+ import { useRouter } from './useRouter'
2
3
 
3
- export function ScriptOnce({
4
- children,
5
- log,
6
- }: {
7
- children: string
8
- log?: boolean
9
- sync?: boolean
10
- }) {
11
- if (typeof document !== 'undefined') {
12
- return null
13
- }
4
+ export const ScriptOnce = Vue.defineComponent({
5
+ name: 'ScriptOnce',
6
+ props: {
7
+ children: {
8
+ type: String,
9
+ required: true,
10
+ },
11
+ },
12
+ setup(props) {
13
+ const router = useRouter()
14
14
 
15
- return (
16
- <script
17
- class="tsr-once"
18
- innerHTML={[
19
- children,
20
- (log ?? true) && process.env.NODE_ENV === 'development'
21
- ? `console.info(\`Injected From Server:
22
- ${jsesc(children.toString(), { quotes: 'backtick' })}\`)`
23
- : '',
24
- 'if (typeof __TSR_SSR__ !== "undefined") __TSR_SSR__.cleanScripts()',
25
- ]
26
- .filter(Boolean)
27
- .join('\n')}
28
- />
29
- )
30
- }
15
+ if (router.isServer) {
16
+ return () => (
17
+ <script
18
+ nonce={router.options.ssr?.nonce}
19
+ class="$tsr"
20
+ innerHTML={props.children}
21
+ />
22
+ )
23
+ }
24
+
25
+ const mounted = Vue.ref(false)
26
+ Vue.onMounted(() => {
27
+ mounted.value = true
28
+ })
29
+
30
+ return () => {
31
+ if (mounted.value) {
32
+ return null
33
+ }
34
+
35
+ return (
36
+ <script
37
+ nonce={router.options.ssr?.nonce}
38
+ class="$tsr"
39
+ data-allow-mismatch
40
+ innerHTML=""
41
+ />
42
+ )
43
+ }
44
+ },
45
+ })
package/src/Scripts.tsx CHANGED
@@ -1,65 +1,130 @@
1
+ import * as Vue from 'vue'
1
2
  import { Asset } from './Asset'
2
3
  import { useRouterState } from './useRouterState'
3
4
  import { useRouter } from './useRouter'
4
5
  import type { RouterManagedTag } from '@tanstack/router-core'
5
6
 
6
- export const Scripts = () => {
7
- const router = useRouter()
7
+ // Script that sets the defer flag for Vue - must run BEFORE TSR bootstrap script
8
+ // This prevents $_TSR.c() from removing scripts until Vue hydration is complete
9
+ const VUE_DEFER_SCRIPT = 'self.$_TSR_DEFER=true'
8
10
 
9
- const assetScripts = useRouterState({
10
- select: (state) => {
11
- const assetScripts: Array<RouterManagedTag> = []
12
- const manifest = router.ssr?.manifest
11
+ export const Scripts = Vue.defineComponent({
12
+ name: 'Scripts',
13
+ setup() {
14
+ const router = useRouter()
15
+ const nonce = router.options.ssr?.nonce
13
16
 
14
- if (!manifest) {
15
- return []
16
- }
17
+ const assetScripts = useRouterState({
18
+ select: (state) => {
19
+ const assetScripts: Array<RouterManagedTag> = []
20
+ const manifest = router.ssr?.manifest
21
+
22
+ if (!manifest) {
23
+ return []
24
+ }
17
25
 
18
- state.matches
19
- .map((match) => router.looseRoutesById[match.routeId]!)
20
- .forEach((route) =>
21
- manifest.routes[route.id]?.assets
22
- ?.filter((d) => d.tag === 'script')
23
- .forEach((asset) => {
24
- assetScripts.push({
25
- tag: 'script',
26
- attrs: asset.attrs,
27
- children: asset.children,
28
- } as any)
29
- }),
30
- )
31
-
32
- return assetScripts
33
- },
34
- })
35
-
36
- const scripts = useRouterState({
37
- select: (state) => ({
38
- scripts: (
39
26
  state.matches
40
- .map((match) => match.scripts!)
41
- .flat(1)
42
- .filter(Boolean) as Array<RouterManagedTag>
43
- ).map(({ children, ...script }) => ({
44
- tag: 'script',
45
- attrs: {
46
- ...script,
47
- },
48
- children,
49
- })),
50
- }),
51
- })
52
-
53
- const allScripts = [
54
- ...scripts.value.scripts,
55
- ...assetScripts.value,
56
- ] as Array<RouterManagedTag>
57
-
58
- return (
59
- <>
60
- {allScripts.map((asset, i) => (
61
- <Asset {...asset} key={i} />
62
- ))}
63
- </>
64
- )
65
- }
27
+ .map((match) => router.looseRoutesById[match.routeId]!)
28
+ .forEach((route) =>
29
+ manifest.routes[route.id]?.assets
30
+ ?.filter((d) => d.tag === 'script')
31
+ .forEach((asset) => {
32
+ assetScripts.push({
33
+ tag: 'script',
34
+ attrs: { ...asset.attrs, nonce },
35
+ children: asset.children,
36
+ } as RouterManagedTag)
37
+ }),
38
+ )
39
+
40
+ return assetScripts
41
+ },
42
+ })
43
+
44
+ const scripts = useRouterState({
45
+ select: (state) => ({
46
+ scripts: (
47
+ state.matches
48
+ .map((match) => match.scripts!)
49
+ .flat(1)
50
+ .filter(Boolean) as Array<RouterManagedTag>
51
+ ).map(({ children, ...script }) => ({
52
+ tag: 'script' as const,
53
+ attrs: {
54
+ ...script,
55
+ nonce,
56
+ },
57
+ children,
58
+ })),
59
+ }),
60
+ })
61
+
62
+ const mounted = Vue.ref(false)
63
+ Vue.onMounted(() => {
64
+ mounted.value = true
65
+ })
66
+
67
+ return () => {
68
+ const allScripts: Array<RouterManagedTag> = []
69
+
70
+ if (router.serverSsr) {
71
+ allScripts.push({
72
+ tag: 'script',
73
+ attrs: { nonce },
74
+ children: VUE_DEFER_SCRIPT,
75
+ } as RouterManagedTag)
76
+
77
+ const serverBufferedScript = router.serverSsr.takeBufferedScripts()
78
+ if (serverBufferedScript) {
79
+ allScripts.push(serverBufferedScript)
80
+ }
81
+ } else if (router.ssr && !mounted.value) {
82
+ allScripts.push({
83
+ tag: 'script',
84
+ attrs: { nonce, 'data-allow-mismatch': true },
85
+ children: '',
86
+ } as RouterManagedTag)
87
+
88
+ allScripts.push({
89
+ tag: 'script',
90
+ attrs: {
91
+ nonce,
92
+ class: '$tsr',
93
+ id: '$tsr-stream-barrier',
94
+ 'data-allow-mismatch': true,
95
+ },
96
+ children: '',
97
+ } as RouterManagedTag)
98
+
99
+ for (const asset of assetScripts.value) {
100
+ allScripts.push({
101
+ tag: 'script',
102
+ attrs: {
103
+ ...asset.attrs,
104
+ 'data-allow-mismatch': true,
105
+ },
106
+ children: '',
107
+ } as RouterManagedTag)
108
+ }
109
+ }
110
+
111
+ for (const script of scripts.value.scripts) {
112
+ allScripts.push(script as RouterManagedTag)
113
+ }
114
+
115
+ if (mounted.value || router.serverSsr) {
116
+ for (const asset of assetScripts.value) {
117
+ allScripts.push(asset)
118
+ }
119
+ }
120
+
121
+ return (
122
+ <>
123
+ {allScripts.map((asset, i) => (
124
+ <Asset {...asset} key={`tsr-scripts-${asset.tag}-${i}`} />
125
+ ))}
126
+ </>
127
+ )
128
+ }
129
+ },
130
+ })