@tanstack/vue-router 1.140.5 → 1.141.0
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/esm/Asset.js +122 -8
- package/dist/esm/Asset.js.map +1 -1
- package/dist/esm/Body.d.ts +4 -0
- package/dist/esm/Body.js +26 -0
- package/dist/esm/Body.js.map +1 -0
- package/dist/esm/CatchBoundary.d.ts +1 -1
- package/dist/esm/CatchBoundary.js +8 -8
- package/dist/esm/CatchBoundary.js.map +1 -1
- package/dist/esm/Html.d.ts +4 -0
- package/dist/esm/Html.js +63 -0
- package/dist/esm/Html.js.map +1 -0
- package/dist/esm/Match.js +87 -49
- package/dist/esm/Match.js.map +1 -1
- package/dist/esm/Matches.js +3 -2
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/RouterProvider.js +3 -0
- package/dist/esm/RouterProvider.js.map +1 -1
- package/dist/esm/ScriptOnce.d.ts +12 -5
- package/dist/esm/ScriptOnce.js +35 -15
- package/dist/esm/ScriptOnce.js.map +1 -1
- package/dist/esm/Scripts.d.ts +2 -1
- package/dist/esm/Scripts.js +101 -35
- package/dist/esm/Scripts.js.map +1 -1
- package/dist/esm/Transitioner.d.ts +16 -0
- package/dist/esm/Transitioner.js +136 -133
- package/dist/esm/Transitioner.js.map +1 -1
- package/dist/esm/awaited.d.ts +20 -5
- package/dist/esm/awaited.js +17 -20
- package/dist/esm/awaited.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lazyRouteComponent.js +2 -2
- package/dist/esm/lazyRouteComponent.js.map +1 -1
- package/dist/esm/link.js +27 -35
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/scroll-restoration.d.ts +8 -1
- package/dist/esm/scroll-restoration.js +44 -12
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/ssr/RouterClient.d.ts +15 -0
- package/dist/esm/ssr/RouterClient.js +46 -0
- package/dist/esm/ssr/RouterClient.js.map +1 -0
- package/dist/esm/ssr/RouterServer.d.ts +15 -0
- package/dist/esm/ssr/RouterServer.js +37 -0
- package/dist/esm/ssr/RouterServer.js.map +1 -0
- package/dist/esm/ssr/client.d.ts +1 -0
- package/dist/esm/ssr/client.js +5 -0
- package/dist/esm/ssr/client.js.map +1 -0
- package/dist/esm/ssr/defaultRenderHandler.d.ts +1 -0
- package/dist/esm/ssr/defaultRenderHandler.js +15 -0
- package/dist/esm/ssr/defaultRenderHandler.js.map +1 -0
- package/dist/esm/ssr/defaultStreamHandler.d.ts +1 -0
- package/dist/esm/ssr/defaultStreamHandler.js +17 -0
- package/dist/esm/ssr/defaultStreamHandler.js.map +1 -0
- package/dist/esm/ssr/renderRouterToStream.d.ts +8 -0
- package/dist/esm/ssr/renderRouterToStream.js +70 -0
- package/dist/esm/ssr/renderRouterToStream.js.map +1 -0
- package/dist/esm/ssr/renderRouterToString.d.ts +7 -0
- package/dist/esm/ssr/renderRouterToString.js +33 -0
- package/dist/esm/ssr/renderRouterToString.js.map +1 -0
- package/dist/esm/ssr/server.d.ts +6 -0
- package/dist/esm/ssr/server.js +14 -0
- package/dist/esm/ssr/server.js.map +1 -0
- package/dist/source/Asset.jsx +119 -7
- package/dist/source/Asset.jsx.map +1 -1
- package/dist/source/Body.d.ts +4 -0
- package/dist/source/Body.jsx +15 -0
- package/dist/source/Body.jsx.map +1 -0
- package/dist/source/CatchBoundary.d.ts +1 -1
- package/dist/source/CatchBoundary.jsx +10 -23
- package/dist/source/CatchBoundary.jsx.map +1 -1
- package/dist/source/Html.d.ts +4 -0
- package/dist/source/Html.jsx +56 -0
- package/dist/source/Html.jsx.map +1 -0
- package/dist/source/Match.jsx +119 -54
- package/dist/source/Match.jsx.map +1 -1
- package/dist/source/Matches.jsx +15 -3
- package/dist/source/Matches.jsx.map +1 -1
- package/dist/source/RouterProvider.jsx +5 -0
- package/dist/source/RouterProvider.jsx.map +1 -1
- package/dist/source/ScriptOnce.d.ts +12 -5
- package/dist/source/ScriptOnce.jsx +27 -16
- package/dist/source/ScriptOnce.jsx.map +1 -1
- package/dist/source/Scripts.d.ts +2 -1
- package/dist/source/Scripts.jsx +100 -42
- package/dist/source/Scripts.jsx.map +1 -1
- package/dist/source/Transitioner.d.ts +16 -0
- package/dist/source/Transitioner.jsx +180 -160
- package/dist/source/Transitioner.jsx.map +1 -1
- package/dist/source/awaited.d.ts +20 -5
- package/dist/source/awaited.jsx +18 -25
- package/dist/source/awaited.jsx.map +1 -1
- package/dist/source/index.d.ts +2 -0
- package/dist/source/index.jsx +2 -0
- package/dist/source/index.jsx.map +1 -1
- package/dist/source/lazyRouteComponent.jsx +4 -2
- package/dist/source/lazyRouteComponent.jsx.map +1 -1
- package/dist/source/link.jsx +37 -51
- package/dist/source/link.jsx.map +1 -1
- package/dist/source/scroll-restoration.d.ts +8 -1
- package/dist/source/scroll-restoration.jsx +55 -12
- package/dist/source/scroll-restoration.jsx.map +1 -1
- package/dist/source/ssr/RouterClient.d.ts +15 -0
- package/dist/source/ssr/RouterClient.jsx +48 -0
- package/dist/source/ssr/RouterClient.jsx.map +1 -0
- package/dist/source/ssr/RouterServer.d.ts +15 -0
- package/dist/source/ssr/RouterServer.jsx +40 -0
- package/dist/source/ssr/RouterServer.jsx.map +1 -0
- package/dist/source/ssr/client.d.ts +1 -0
- package/dist/source/ssr/client.js +2 -0
- package/dist/source/ssr/client.js.map +1 -0
- package/dist/source/ssr/defaultRenderHandler.d.ts +1 -0
- package/dist/source/ssr/defaultRenderHandler.jsx +9 -0
- package/dist/source/ssr/defaultRenderHandler.jsx.map +1 -0
- package/dist/source/ssr/defaultStreamHandler.d.ts +1 -0
- package/dist/source/ssr/defaultStreamHandler.jsx +10 -0
- package/dist/source/ssr/defaultStreamHandler.jsx.map +1 -0
- package/dist/source/ssr/renderRouterToStream.d.ts +8 -0
- package/dist/source/ssr/renderRouterToStream.jsx +55 -0
- package/dist/source/ssr/renderRouterToStream.jsx.map +1 -0
- package/dist/source/ssr/renderRouterToString.d.ts +7 -0
- package/dist/source/ssr/renderRouterToString.jsx +26 -0
- package/dist/source/ssr/renderRouterToString.jsx.map +1 -0
- package/dist/source/ssr/server.d.ts +6 -0
- package/dist/source/ssr/server.js +7 -0
- package/dist/source/ssr/server.js.map +1 -0
- package/package.json +16 -3
- package/src/Asset.tsx +157 -7
- package/src/Body.tsx +26 -0
- package/src/CatchBoundary.tsx +11 -25
- package/src/Html.tsx +65 -0
- package/src/Match.tsx +135 -58
- package/src/Matches.tsx +16 -4
- package/src/RouterProvider.tsx +6 -0
- package/src/ScriptOnce.tsx +43 -28
- package/src/Scripts.tsx +121 -56
- package/src/Transitioner.tsx +197 -176
- package/src/awaited.tsx +17 -28
- package/src/index.tsx +2 -0
- package/src/lazyRouteComponent.tsx +4 -2
- package/src/link.tsx +42 -47
- package/src/scroll-restoration.tsx +69 -21
- package/src/ssr/RouterClient.tsx +58 -0
- package/src/ssr/RouterServer.tsx +51 -0
- package/src/ssr/client.ts +1 -0
- package/src/ssr/defaultRenderHandler.tsx +12 -0
- package/src/ssr/defaultStreamHandler.tsx +13 -0
- package/src/ssr/renderRouterToStream.tsx +85 -0
- package/src/ssr/renderRouterToString.tsx +37 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
(
|
|
86
|
-
|
|
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:
|
|
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 !==
|
|
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: () =>
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
//
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 (!
|
|
249
|
-
return router.routesById[
|
|
314
|
+
if (!combinedState.value) return null
|
|
315
|
+
return router.routesById[combinedState.value.routeId]!
|
|
250
316
|
})
|
|
251
317
|
|
|
252
|
-
const match = Vue.computed(() =>
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
466
|
+
if (!childMatchData.value) {
|
|
393
467
|
return null
|
|
394
468
|
}
|
|
395
469
|
|
|
396
|
-
const nextMatch = Vue.h(Match, {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
package/src/RouterProvider.tsx
CHANGED
|
@@ -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
|
}
|
package/src/ScriptOnce.tsx
CHANGED
|
@@ -1,30 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Vue from 'vue'
|
|
2
|
+
import { useRouter } from './useRouter'
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
})
|