@tanstack/vue-router 1.141.2 → 1.141.6

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 (56) hide show
  1. package/dist/esm/CatchBoundary.d.ts +4 -2
  2. package/dist/esm/CatchBoundary.js +29 -28
  3. package/dist/esm/CatchBoundary.js.map +1 -1
  4. package/dist/esm/ClientOnly.js +33 -0
  5. package/dist/esm/ClientOnly.js.map +1 -0
  6. package/dist/esm/Match.js +66 -39
  7. package/dist/esm/Match.js.map +1 -1
  8. package/dist/esm/ScriptOnce.js +1 -3
  9. package/dist/esm/ScriptOnce.js.map +1 -1
  10. package/dist/esm/Scripts.js +0 -9
  11. package/dist/esm/Scripts.js.map +1 -1
  12. package/dist/esm/Transitioner.js +20 -6
  13. package/dist/esm/Transitioner.js.map +1 -1
  14. package/dist/esm/index.d.ts +2 -1
  15. package/dist/esm/index.js +2 -0
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/lazyRouteComponent.d.ts +0 -6
  18. package/dist/esm/lazyRouteComponent.js +5 -24
  19. package/dist/esm/lazyRouteComponent.js.map +1 -1
  20. package/dist/esm/link.d.ts +4 -0
  21. package/dist/esm/link.js.map +1 -1
  22. package/dist/esm/route.d.ts +6 -1
  23. package/dist/esm/route.js +25 -0
  24. package/dist/esm/route.js.map +1 -1
  25. package/dist/source/CatchBoundary.d.ts +4 -2
  26. package/dist/source/CatchBoundary.jsx +23 -22
  27. package/dist/source/CatchBoundary.jsx.map +1 -1
  28. package/dist/source/Match.jsx +90 -63
  29. package/dist/source/Match.jsx.map +1 -1
  30. package/dist/source/ScriptOnce.jsx +2 -2
  31. package/dist/source/ScriptOnce.jsx.map +1 -1
  32. package/dist/source/Scripts.jsx +0 -9
  33. package/dist/source/Scripts.jsx.map +1 -1
  34. package/dist/source/Transitioner.jsx +23 -16
  35. package/dist/source/Transitioner.jsx.map +1 -1
  36. package/dist/source/index.d.ts +2 -1
  37. package/dist/source/index.jsx +1 -0
  38. package/dist/source/index.jsx.map +1 -1
  39. package/dist/source/lazyRouteComponent.d.ts +0 -6
  40. package/dist/source/lazyRouteComponent.jsx +3 -23
  41. package/dist/source/lazyRouteComponent.jsx.map +1 -1
  42. package/dist/source/link.d.ts +4 -0
  43. package/dist/source/link.jsx.map +1 -1
  44. package/dist/source/route.d.ts +6 -1
  45. package/dist/source/route.js +13 -0
  46. package/dist/source/route.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/CatchBoundary.tsx +35 -32
  49. package/src/Match.tsx +115 -73
  50. package/src/ScriptOnce.tsx +1 -3
  51. package/src/Scripts.tsx +0 -11
  52. package/src/Transitioner.tsx +31 -17
  53. package/src/index.tsx +2 -0
  54. package/src/lazyRouteComponent.tsx +10 -32
  55. package/src/link.tsx +20 -0
  56. package/src/route.ts +33 -1
package/src/Scripts.tsx CHANGED
@@ -4,10 +4,6 @@ import { useRouterState } from './useRouterState'
4
4
  import { useRouter } from './useRouter'
5
5
  import type { RouterManagedTag } from '@tanstack/router-core'
6
6
 
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'
10
-
11
7
  export const Scripts = Vue.defineComponent({
12
8
  name: 'Scripts',
13
9
  setup() {
@@ -68,12 +64,6 @@ export const Scripts = Vue.defineComponent({
68
64
  const allScripts: Array<RouterManagedTag> = []
69
65
 
70
66
  if (router.serverSsr) {
71
- allScripts.push({
72
- tag: 'script',
73
- attrs: { nonce },
74
- children: VUE_DEFER_SCRIPT,
75
- } as RouterManagedTag)
76
-
77
67
  const serverBufferedScript = router.serverSsr.takeBufferedScripts()
78
68
  if (serverBufferedScript) {
79
69
  allScripts.push(serverBufferedScript)
@@ -89,7 +79,6 @@ export const Scripts = Vue.defineComponent({
89
79
  tag: 'script',
90
80
  attrs: {
91
81
  nonce,
92
- class: '$tsr',
93
82
  id: '$tsr-stream-barrier',
94
83
  'data-allow-mismatch': true,
95
84
  },
@@ -88,18 +88,23 @@ export function useTransitionerSetup() {
88
88
  endTransition()
89
89
  }
90
90
 
91
- // For Vue, we need to completely override startViewTransition because Vue's
92
- // async rendering doesn't work well with the View Transitions API's requirement
93
- // for synchronous DOM updates. The browser expects the DOM to be updated
94
- // when the callback promise resolves, but Vue updates asynchronously.
95
- //
96
- // Our approach: Skip the actual view transition animation but still update state.
97
- // This ensures navigation works correctly even without the visual transition.
98
- // In the future, we could explore using viewTransition.captured like vue-view-transitions does.
91
+ // Vue updates DOM asynchronously (next tick). The View Transitions API expects the
92
+ // update callback promise to resolve only after the DOM has been updated.
93
+ // Wrap the router-core implementation to await a Vue flush before resolving.
94
+ const originalStartViewTransition:
95
+ | undefined
96
+ | ((fn: () => Promise<void>) => void) =
97
+ (router as any).__tsrOriginalStartViewTransition ??
98
+ router.startViewTransition
99
+
100
+ ;(router as any).__tsrOriginalStartViewTransition =
101
+ originalStartViewTransition
102
+
99
103
  router.startViewTransition = (fn: () => Promise<void>) => {
100
- // Just run the callback directly without wrapping in document.startViewTransition
101
- // This ensures the state updates happen and Vue can render them normally
102
- fn()
104
+ return originalStartViewTransition?.(async () => {
105
+ await fn()
106
+ await Vue.nextTick()
107
+ })
103
108
  }
104
109
 
105
110
  // Subscribe to location changes
@@ -131,6 +136,13 @@ export function useTransitionerSetup() {
131
136
 
132
137
  Vue.onMounted(() => {
133
138
  isMounted.value = true
139
+ if (!isAnyPending.value) {
140
+ router.__store.setState((s) =>
141
+ s.status === 'pending'
142
+ ? { ...s, status: 'idle', resolvedLocation: s.location }
143
+ : s,
144
+ )
145
+ }
134
146
  })
135
147
 
136
148
  Vue.onUnmounted(() => {
@@ -196,6 +208,14 @@ export function useTransitionerSetup() {
196
208
  Vue.watch(isAnyPending, (newValue) => {
197
209
  if (!isMounted.value) return
198
210
  try {
211
+ if (!newValue && router.__store.state.status === 'pending') {
212
+ router.__store.setState((s) => ({
213
+ ...s,
214
+ status: 'idle',
215
+ resolvedLocation: s.location,
216
+ }))
217
+ }
218
+
199
219
  // The router was pending and now it's not
200
220
  if (previousIsAnyPending.value.previous && !newValue) {
201
221
  const changeInfo = getLocationChangeInfo(router.state)
@@ -204,12 +224,6 @@ export function useTransitionerSetup() {
204
224
  ...changeInfo,
205
225
  })
206
226
 
207
- router.__store.setState((s) => ({
208
- ...s,
209
- status: 'idle',
210
- resolvedLocation: s.location,
211
- }))
212
-
213
227
  if (changeInfo.hrefChanged) {
214
228
  handleHashScroll(router)
215
229
  }
package/src/index.tsx CHANGED
@@ -219,6 +219,7 @@ export type {
219
219
  ActiveLinkOptions,
220
220
  LinkProps,
221
221
  LinkComponent,
222
+ LinkComponentRoute,
222
223
  LinkComponentProps,
223
224
  CreateLinkProps,
224
225
  } from './link'
@@ -346,3 +347,4 @@ export type {
346
347
  LocationRewrite,
347
348
  LocationRewriteFunction,
348
349
  } from '@tanstack/router-core'
350
+ export { ClientOnly } from './ClientOnly'
@@ -1,5 +1,6 @@
1
1
  import * as Vue from 'vue'
2
2
  import { Outlet } from './Match'
3
+ import { ClientOnly } from './ClientOnly'
3
4
  import type { AsyncRouteComponent } from './route'
4
5
 
5
6
  // If the load fails due to module not found, it may mean a new version of
@@ -19,34 +20,6 @@ function isModuleNotFoundError(error: any): boolean {
19
20
  )
20
21
  }
21
22
 
22
- export function ClientOnly(props: { children?: any; fallback?: Vue.VNode }) {
23
- const hydrated = useHydrated()
24
-
25
- return () => {
26
- if (hydrated.value) {
27
- return props.children
28
- }
29
- return props.fallback || null
30
- }
31
- }
32
-
33
- export function useHydrated() {
34
- // Only hydrate on client-side, never on server
35
- const hydrated = Vue.ref(false)
36
-
37
- // If on server, return false
38
- if (typeof window === 'undefined') {
39
- return Vue.computed(() => false)
40
- }
41
-
42
- // On client, set to true once mounted
43
- Vue.onMounted(() => {
44
- hydrated.value = true
45
- })
46
-
47
- return hydrated
48
- }
49
-
50
23
  export function lazyRouteComponent<
51
24
  T extends Record<string, any>,
52
25
  TKey extends keyof T = 'default',
@@ -156,10 +129,15 @@ export function lazyRouteComponent<
156
129
 
157
130
  // If SSR is disabled for this component
158
131
  if (ssr?.() === false) {
159
- return Vue.h(ClientOnly, {
160
- fallback: Vue.h(Outlet),
161
- children: Vue.h(component.value, props),
162
- })
132
+ return Vue.h(
133
+ ClientOnly,
134
+ {
135
+ fallback: Vue.h(Outlet),
136
+ },
137
+ {
138
+ default: () => Vue.h(component.value, props),
139
+ },
140
+ )
163
141
  }
164
142
 
165
143
  // Regular render with the loaded component
package/src/link.tsx CHANGED
@@ -630,6 +630,26 @@ export type LinkComponent<TComp> = <
630
630
  props: LinkComponentProps<TComp, TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
631
631
  ) => Vue.VNode
632
632
 
633
+ export interface LinkComponentRoute<
634
+ in out TDefaultFrom extends string = string,
635
+ > {
636
+ defaultFrom: TDefaultFrom
637
+ <
638
+ TRouter extends AnyRouter = RegisteredRouter,
639
+ const TTo extends string | undefined = undefined,
640
+ const TMaskTo extends string = '',
641
+ >(
642
+ props: LinkComponentProps<
643
+ 'a',
644
+ TRouter,
645
+ this['defaultFrom'],
646
+ TTo,
647
+ this['defaultFrom'],
648
+ TMaskTo
649
+ >,
650
+ ): Vue.VNode
651
+ }
652
+
633
653
  export function createLink<const TComp>(
634
654
  Comp: Constrain<TComp, any, (props: CreateLinkProps) => Vue.VNode>,
635
655
  ): LinkComponent<TComp> {
package/src/route.ts CHANGED
@@ -4,6 +4,8 @@ import {
4
4
  BaseRouteApi,
5
5
  notFound,
6
6
  } from '@tanstack/router-core'
7
+ import * as Vue from 'vue'
8
+ import { Link } from './link'
7
9
  import { useLoaderData } from './useLoaderData'
8
10
  import { useLoaderDeps } from './useLoaderDeps'
9
11
  import { useParams } from './useParams'
@@ -42,8 +44,8 @@ import type { UseMatchRoute } from './useMatch'
42
44
  import type { UseLoaderDepsRoute } from './useLoaderDeps'
43
45
  import type { UseParamsRoute } from './useParams'
44
46
  import type { UseSearchRoute } from './useSearch'
45
- import type * as Vue from 'vue'
46
47
  import type { UseRouteContextRoute } from './useRouteContext'
48
+ import type { LinkComponentRoute } from './link'
47
49
 
48
50
  // Structural type for Vue SFC components (.vue files)
49
51
  // Uses structural matching to accept Vue components without breaking
@@ -73,6 +75,7 @@ declare module '@tanstack/router-core' {
73
75
  useLoaderDeps: UseLoaderDepsRoute<TId>
74
76
  useLoaderData: UseLoaderDataRoute<TId>
75
77
  useNavigate: () => UseNavigateResult<TFullPath>
78
+ Link: LinkComponentRoute<TFullPath>
76
79
  }
77
80
  }
78
81
 
@@ -140,6 +143,19 @@ export class RouteApi<
140
143
  notFound = (opts?: NotFoundError) => {
141
144
  return notFound({ routeId: this.id as string, ...opts })
142
145
  }
146
+
147
+ Link: LinkComponentRoute<RouteTypesById<TRouter, TId>['fullPath']> = ((
148
+ props,
149
+ ctx?: Vue.SetupContext,
150
+ ) => {
151
+ const router = useRouter()
152
+ const fullPath = router.routesById[this.id as string].fullPath
153
+ return Vue.h(
154
+ Link as any,
155
+ { from: fullPath as never, ...(props as any) },
156
+ ctx?.slots,
157
+ )
158
+ }) as LinkComponentRoute<RouteTypesById<TRouter, TId>['fullPath']>
143
159
  }
144
160
 
145
161
  export class Route<
@@ -277,6 +293,14 @@ export class Route<
277
293
  useNavigate = (): UseNavigateResult<TFullPath> => {
278
294
  return useNavigate({ from: this.fullPath })
279
295
  }
296
+
297
+ Link: LinkComponentRoute<TFullPath> = ((props, ctx?: Vue.SetupContext) => {
298
+ return Vue.h(
299
+ Link as any,
300
+ { from: this.fullPath as never, ...(props as any) },
301
+ ctx?.slots,
302
+ )
303
+ }) as LinkComponentRoute<TFullPath>
280
304
  }
281
305
 
282
306
  export function createRoute<
@@ -515,6 +539,14 @@ export class RootRoute<
515
539
  useNavigate = (): UseNavigateResult<'/'> => {
516
540
  return useNavigate({ from: this.fullPath })
517
541
  }
542
+
543
+ Link: LinkComponentRoute<'/'> = ((props, ctx?: Vue.SetupContext) => {
544
+ return Vue.h(
545
+ Link as any,
546
+ { from: this.fullPath as never, ...(props as any) },
547
+ ctx?.slots,
548
+ )
549
+ }) as LinkComponentRoute<'/'>
518
550
  }
519
551
 
520
552
  export function createRouteMask<