@tanstack/vue-router 1.167.5 → 1.168.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.
Files changed (95) hide show
  1. package/dist/esm/Match.js +55 -61
  2. package/dist/esm/Match.js.map +1 -1
  3. package/dist/esm/Matches.js +8 -15
  4. package/dist/esm/Matches.js.map +1 -1
  5. package/dist/esm/Scripts.js +7 -6
  6. package/dist/esm/Scripts.js.map +1 -1
  7. package/dist/esm/Transitioner.js +18 -24
  8. package/dist/esm/Transitioner.js.map +1 -1
  9. package/dist/esm/headContentUtils.js +13 -15
  10. package/dist/esm/headContentUtils.js.map +1 -1
  11. package/dist/esm/index.dev.js +6 -6
  12. package/dist/esm/index.js +6 -6
  13. package/dist/esm/link.js +242 -178
  14. package/dist/esm/link.js.map +1 -1
  15. package/dist/esm/matchContext.d.ts +8 -14
  16. package/dist/esm/matchContext.js +11 -9
  17. package/dist/esm/matchContext.js.map +1 -1
  18. package/dist/esm/not-found.js +6 -3
  19. package/dist/esm/not-found.js.map +1 -1
  20. package/dist/esm/router.js +2 -1
  21. package/dist/esm/router.js.map +1 -1
  22. package/dist/esm/routerStores.d.ts +13 -0
  23. package/dist/esm/routerStores.js +33 -0
  24. package/dist/esm/routerStores.js.map +1 -0
  25. package/dist/esm/ssr/RouterClient.js +1 -1
  26. package/dist/esm/ssr/RouterClient.js.map +1 -1
  27. package/dist/esm/ssr/renderRouterToStream.js +2 -2
  28. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  29. package/dist/esm/ssr/renderRouterToString.js +1 -1
  30. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  31. package/dist/esm/useCanGoBack.d.ts +1 -1
  32. package/dist/esm/useCanGoBack.js +3 -2
  33. package/dist/esm/useCanGoBack.js.map +1 -1
  34. package/dist/esm/useLocation.js +3 -2
  35. package/dist/esm/useLocation.js.map +1 -1
  36. package/dist/esm/useMatch.js +29 -19
  37. package/dist/esm/useMatch.js.map +1 -1
  38. package/dist/esm/useRouterState.js +4 -4
  39. package/dist/esm/useRouterState.js.map +1 -1
  40. package/dist/source/Match.jsx +121 -159
  41. package/dist/source/Match.jsx.map +1 -1
  42. package/dist/source/Matches.jsx +11 -28
  43. package/dist/source/Matches.jsx.map +1 -1
  44. package/dist/source/Scripts.jsx +32 -35
  45. package/dist/source/Scripts.jsx.map +1 -1
  46. package/dist/source/Transitioner.jsx +19 -21
  47. package/dist/source/Transitioner.jsx.map +1 -1
  48. package/dist/source/headContentUtils.jsx +51 -61
  49. package/dist/source/headContentUtils.jsx.map +1 -1
  50. package/dist/source/link.jsx +298 -249
  51. package/dist/source/link.jsx.map +1 -1
  52. package/dist/source/matchContext.d.ts +8 -14
  53. package/dist/source/matchContext.jsx +17 -23
  54. package/dist/source/matchContext.jsx.map +1 -1
  55. package/dist/source/not-found.jsx +6 -5
  56. package/dist/source/not-found.jsx.map +1 -1
  57. package/dist/source/router.js +2 -1
  58. package/dist/source/router.js.map +1 -1
  59. package/dist/source/routerStores.d.ts +13 -0
  60. package/dist/source/routerStores.js +37 -0
  61. package/dist/source/routerStores.js.map +1 -0
  62. package/dist/source/ssr/RouterClient.jsx +1 -1
  63. package/dist/source/ssr/RouterClient.jsx.map +1 -1
  64. package/dist/source/ssr/renderRouterToStream.jsx +2 -2
  65. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -1
  66. package/dist/source/ssr/renderRouterToString.jsx +1 -1
  67. package/dist/source/ssr/renderRouterToString.jsx.map +1 -1
  68. package/dist/source/useCanGoBack.d.ts +1 -1
  69. package/dist/source/useCanGoBack.js +4 -2
  70. package/dist/source/useCanGoBack.js.map +1 -1
  71. package/dist/source/useLocation.jsx +4 -4
  72. package/dist/source/useLocation.jsx.map +1 -1
  73. package/dist/source/useMatch.jsx +60 -38
  74. package/dist/source/useMatch.jsx.map +1 -1
  75. package/dist/source/useRouterState.jsx +4 -4
  76. package/dist/source/useRouterState.jsx.map +1 -1
  77. package/package.json +2 -2
  78. package/skills/vue-router/SKILL.md +3 -0
  79. package/src/Match.tsx +168 -180
  80. package/src/Matches.tsx +18 -31
  81. package/src/Scripts.tsx +40 -40
  82. package/src/Transitioner.tsx +35 -23
  83. package/src/headContentUtils.tsx +101 -107
  84. package/src/link.tsx +445 -300
  85. package/src/matchContext.tsx +23 -25
  86. package/src/not-found.tsx +9 -5
  87. package/src/router.ts +2 -1
  88. package/src/routerStores.ts +54 -0
  89. package/src/ssr/RouterClient.tsx +1 -1
  90. package/src/ssr/renderRouterToStream.tsx +2 -2
  91. package/src/ssr/renderRouterToString.tsx +1 -1
  92. package/src/useCanGoBack.ts +7 -2
  93. package/src/useLocation.tsx +8 -5
  94. package/src/useMatch.tsx +95 -49
  95. package/src/useRouterState.tsx +6 -4
@@ -1,9 +1,9 @@
1
1
  import * as Vue from 'vue';
2
2
  import { deepEqual, exactPathTest, isDangerousProtocol, preloadWarning, removeTrailingSlash, } from '@tanstack/router-core';
3
- import { useRouterState } from './useRouterState';
3
+ import { isServer } from '@tanstack/router-core/isServer';
4
+ import { useStore } from '@tanstack/vue-store';
4
5
  import { useRouter } from './useRouter';
5
6
  import { useIntersectionObserver } from './utils';
6
- import { useMatches } from './Matches';
7
7
  const timeoutMap = new WeakMap();
8
8
  export function useLinkProps(options) {
9
9
  const router = useRouter();
@@ -24,134 +24,8 @@ export function useLinkProps(options) {
24
24
  return 'internal';
25
25
  }
26
26
  });
27
- const buildLocationKey = useRouterState({
28
- select: (s) => {
29
- const leaf = s.matches[s.matches.length - 1];
30
- return {
31
- search: leaf?.search,
32
- hash: s.location.hash,
33
- path: leaf?.pathname, // path + params
34
- };
35
- },
36
- });
37
- // when `from` is not supplied, use the leaf route of the current matches as the `from` location
38
- const from = useMatches({
39
- select: (matches) => options.from ?? matches[matches.length - 1]?.fullPath,
40
- });
41
- const _options = Vue.computed(() => ({
42
- ...options,
43
- from: from.value,
44
- }));
45
- const next = Vue.computed(() => {
46
- // Depend on search to rebuild when search changes
47
- buildLocationKey.value;
48
- return router.buildLocation(_options.value);
49
- });
50
- const preload = Vue.computed(() => {
51
- if (_options.value.reloadDocument) {
52
- return false;
53
- }
54
- return options.preload ?? router.options.defaultPreload;
55
- });
56
- const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
57
- const isActive = useRouterState({
58
- select: (s) => {
59
- const activeOptions = options.activeOptions;
60
- if (activeOptions?.exact) {
61
- const testExact = exactPathTest(s.location.pathname, next.value.pathname, router.basepath);
62
- if (!testExact) {
63
- return false;
64
- }
65
- }
66
- else {
67
- const currentPathSplit = removeTrailingSlash(s.location.pathname, router.basepath).split('/');
68
- const nextPathSplit = removeTrailingSlash(next.value?.pathname, router.basepath)?.split('/');
69
- const pathIsFuzzyEqual = nextPathSplit?.every((d, i) => d === currentPathSplit[i]);
70
- if (!pathIsFuzzyEqual) {
71
- return false;
72
- }
73
- }
74
- if (activeOptions?.includeSearch ?? true) {
75
- const searchTest = deepEqual(s.location.search, next.value.search, {
76
- partial: !activeOptions?.exact,
77
- ignoreUndefined: !activeOptions?.explicitUndefined,
78
- });
79
- if (!searchTest) {
80
- return false;
81
- }
82
- }
83
- if (activeOptions?.includeHash) {
84
- return s.location.hash === next.value.hash;
85
- }
86
- return true;
87
- },
88
- });
89
- const doPreload = () => router.preloadRoute(_options.value).catch((err) => {
90
- console.warn(err);
91
- console.warn(preloadWarning);
92
- });
93
- const preloadViewportIoCallback = (entry) => {
94
- if (entry?.isIntersecting) {
95
- doPreload();
96
- }
97
- };
98
27
  const ref = Vue.ref(null);
99
- useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: '100px' }, { disabled: () => !!options.disabled || !(preload.value === 'viewport') });
100
- Vue.effect(() => {
101
- if (hasRenderFetched) {
102
- return;
103
- }
104
- if (!options.disabled && preload.value === 'render') {
105
- doPreload();
106
- hasRenderFetched = true;
107
- }
108
- });
109
- // Create safe props that can be spread
110
- const getPropsSafeToSpread = () => {
111
- const result = {};
112
- const optionRecord = options;
113
- for (const key in options) {
114
- if (![
115
- 'activeProps',
116
- 'inactiveProps',
117
- 'activeOptions',
118
- 'to',
119
- 'preload',
120
- 'preloadDelay',
121
- 'hashScrollIntoView',
122
- 'replace',
123
- 'startTransition',
124
- 'resetScroll',
125
- 'viewTransition',
126
- 'children',
127
- 'target',
128
- 'disabled',
129
- 'style',
130
- 'class',
131
- 'onClick',
132
- 'onBlur',
133
- 'onFocus',
134
- 'onMouseEnter',
135
- 'onMouseLeave',
136
- 'onMouseOver',
137
- 'onMouseOut',
138
- 'onTouchStart',
139
- 'ignoreBlocker',
140
- 'params',
141
- 'search',
142
- 'hash',
143
- 'state',
144
- 'mask',
145
- 'reloadDocument',
146
- '_asChild',
147
- 'from',
148
- 'additionalProps',
149
- ].includes(key)) {
150
- result[key] = optionRecord[key];
151
- }
152
- }
153
- return result;
154
- };
28
+ const eventHandlers = getLinkEventHandlers(options);
155
29
  if (type.value === 'external') {
156
30
  // Block dangerous protocols like javascript:, blob:, data:
157
31
  if (isDangerousProtocol(options.to, router.protocolAllowlist)) {
@@ -160,7 +34,7 @@ export function useLinkProps(options) {
160
34
  }
161
35
  // Return props without href to prevent navigation
162
36
  const safeProps = {
163
- ...getPropsSafeToSpread(),
37
+ ...getPropsSafeToSpread(options),
164
38
  ref,
165
39
  // No href attribute - blocks the dangerous protocol
166
40
  target: options.target,
@@ -170,11 +44,11 @@ export function useLinkProps(options) {
170
44
  onClick: options.onClick,
171
45
  onBlur: options.onBlur,
172
46
  onFocus: options.onFocus,
173
- onMouseEnter: options.onMouseEnter,
174
- onMouseLeave: options.onMouseLeave,
175
- onMouseOver: options.onMouseOver,
176
- onMouseOut: options.onMouseOut,
177
- onTouchStart: options.onTouchStart,
47
+ onMouseenter: eventHandlers.onMouseenter,
48
+ onMouseleave: eventHandlers.onMouseleave,
49
+ onMouseover: eventHandlers.onMouseover,
50
+ onMouseout: eventHandlers.onMouseout,
51
+ onTouchstart: eventHandlers.onTouchstart,
178
52
  };
179
53
  // Remove undefined values
180
54
  Object.keys(safeProps).forEach((key) => {
@@ -186,7 +60,7 @@ export function useLinkProps(options) {
186
60
  }
187
61
  // External links just have simple props
188
62
  const externalProps = {
189
- ...getPropsSafeToSpread(),
63
+ ...getPropsSafeToSpread(options),
190
64
  ref,
191
65
  href: options.to,
192
66
  target: options.target,
@@ -196,11 +70,11 @@ export function useLinkProps(options) {
196
70
  onClick: options.onClick,
197
71
  onBlur: options.onBlur,
198
72
  onFocus: options.onFocus,
199
- onMouseEnter: options.onMouseEnter,
200
- onMouseLeave: options.onMouseLeave,
201
- onMouseOver: options.onMouseOver,
202
- onMouseOut: options.onMouseOut,
203
- onTouchStart: options.onTouchStart,
73
+ onMouseenter: eventHandlers.onMouseenter,
74
+ onMouseleave: eventHandlers.onMouseleave,
75
+ onMouseover: eventHandlers.onMouseover,
76
+ onMouseout: eventHandlers.onMouseout,
77
+ onTouchstart: eventHandlers.onTouchstart,
204
78
  };
205
79
  // Remove undefined values
206
80
  Object.keys(externalProps).forEach((key) => {
@@ -210,6 +84,79 @@ export function useLinkProps(options) {
210
84
  });
211
85
  return Vue.computed(() => externalProps);
212
86
  }
87
+ // During SSR we render exactly once and do not need reactivity.
88
+ // Avoid store subscriptions, effects and observers on the server.
89
+ if (isServer ?? router.isServer) {
90
+ const next = router.buildLocation(options);
91
+ const href = getHref({
92
+ options: options,
93
+ router,
94
+ nextLocation: next,
95
+ });
96
+ const isActive = getIsActive({
97
+ loc: router.stores.location.state,
98
+ nextLoc: next,
99
+ activeOptions: options.activeOptions,
100
+ router,
101
+ });
102
+ const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolveStyleProps({
103
+ options: options,
104
+ isActive,
105
+ });
106
+ const result = combineResultProps({
107
+ href,
108
+ options: options,
109
+ isActive,
110
+ isTransitioning: false,
111
+ resolvedActiveProps,
112
+ resolvedInactiveProps,
113
+ resolvedClassName,
114
+ resolvedStyle,
115
+ });
116
+ return Vue.ref(result);
117
+ }
118
+ const currentLocation = useStore(router.stores.location, (l) => l, {
119
+ equal: (prev, next) => prev.href === next.href,
120
+ });
121
+ const next = Vue.computed(() => {
122
+ // Rebuild when inherited search/hash or the current route context changes.
123
+ const opts = { _fromLocation: currentLocation.value, ...options };
124
+ return router.buildLocation(opts);
125
+ });
126
+ const preload = Vue.computed(() => {
127
+ if (options.reloadDocument) {
128
+ return false;
129
+ }
130
+ return options.preload ?? router.options.defaultPreload;
131
+ });
132
+ const preloadDelay = Vue.computed(() => options.preloadDelay ?? router.options.defaultPreloadDelay ?? 0);
133
+ const isActive = Vue.computed(() => getIsActive({
134
+ activeOptions: options.activeOptions,
135
+ loc: currentLocation.value,
136
+ nextLoc: next.value,
137
+ router,
138
+ }));
139
+ const doPreload = () => router
140
+ .preloadRoute({ ...options, _builtLocation: next.value })
141
+ .catch((err) => {
142
+ console.warn(err);
143
+ console.warn(preloadWarning);
144
+ });
145
+ const preloadViewportIoCallback = (entry) => {
146
+ if (entry?.isIntersecting) {
147
+ doPreload();
148
+ }
149
+ };
150
+ useIntersectionObserver(ref, preloadViewportIoCallback, { rootMargin: '100px' }, { disabled: () => !!options.disabled || !(preload.value === 'viewport') });
151
+ Vue.effect(() => {
152
+ if (hasRenderFetched) {
153
+ return;
154
+ }
155
+ if (!options.disabled && preload.value === 'render') {
156
+ doPreload();
157
+ hasRenderFetched = true;
158
+ }
159
+ });
213
160
  // The click handler
214
161
  const handleClick = (e) => {
215
162
  // Check actual element's target attribute as fallback
@@ -221,7 +168,7 @@ export function useLinkProps(options) {
221
168
  (!effectiveTarget || effectiveTarget === '_self') &&
222
169
  e.button === 0) {
223
170
  // Don't prevent default or handle navigation if reloadDocument is true
224
- if (_options.value.reloadDocument) {
171
+ if (options.reloadDocument) {
225
172
  return;
226
173
  }
227
174
  e.preventDefault();
@@ -232,7 +179,7 @@ export function useLinkProps(options) {
232
179
  });
233
180
  // All is well? Navigate!
234
181
  router.navigate({
235
- ..._options.value,
182
+ ...options,
236
183
  replace: options.replace,
237
184
  resetScroll: options.resetScroll,
238
185
  hashScrollIntoView: options.hashScrollIntoView,
@@ -283,64 +230,15 @@ export function useLinkProps(options) {
283
230
  };
284
231
  }
285
232
  // Get the active and inactive props
286
- const resolvedActiveProps = Vue.computed(() => {
287
- const activeProps = options.activeProps || (() => ({ class: 'active' }));
288
- const props = isActive.value
289
- ? typeof activeProps === 'function'
290
- ? activeProps()
291
- : activeProps
292
- : {};
293
- return props || { class: undefined, style: undefined };
294
- });
295
- const resolvedInactiveProps = Vue.computed(() => {
296
- const inactiveProps = options.inactiveProps || (() => ({}));
297
- const props = isActive.value
298
- ? {}
299
- : typeof inactiveProps === 'function'
300
- ? inactiveProps()
301
- : inactiveProps;
302
- return props || { class: undefined, style: undefined };
303
- });
304
- const resolvedClassName = Vue.computed(() => {
305
- const classes = [
306
- options.class,
307
- resolvedActiveProps.value?.class,
308
- resolvedInactiveProps.value?.class,
309
- ].filter(Boolean);
310
- return classes.length ? classes.join(' ') : undefined;
311
- });
312
- const resolvedStyle = Vue.computed(() => {
313
- const result = {};
314
- // Merge styles from all sources
315
- if (options.style) {
316
- Object.assign(result, options.style);
317
- }
318
- if (resolvedActiveProps.value?.style) {
319
- Object.assign(result, resolvedActiveProps.value.style);
320
- }
321
- if (resolvedInactiveProps.value?.style) {
322
- Object.assign(result, resolvedInactiveProps.value.style);
323
- }
324
- return Object.keys(result).length > 0 ? result : undefined;
325
- });
326
- const href = Vue.computed(() => {
327
- if (options.disabled) {
328
- return undefined;
329
- }
330
- const nextLocation = next.value;
331
- const location = nextLocation?.maskedLocation ?? nextLocation;
332
- // Use publicHref - it contains the correct href for display
333
- // When a rewrite changes the origin, publicHref is the full URL
334
- // Otherwise it's the origin-stripped path
335
- // This avoids constructing URL objects in the hot path
336
- const publicHref = location?.publicHref;
337
- if (!publicHref)
338
- return undefined;
339
- const external = location?.external;
340
- if (external)
341
- return publicHref;
342
- return router.history.createHref(publicHref) || '/';
343
- });
233
+ const resolvedStyleProps = Vue.computed(() => resolveStyleProps({
234
+ options: options,
235
+ isActive: isActive.value,
236
+ }));
237
+ const href = Vue.computed(() => getHref({
238
+ options: options,
239
+ router,
240
+ nextLocation: next.value,
241
+ }));
344
242
  // Create static event handlers that don't change between renders
345
243
  const staticEventHandlers = {
346
244
  onClick: composeEventHandlers([
@@ -356,76 +254,229 @@ export function useLinkProps(options) {
356
254
  enqueueIntentPreload,
357
255
  ]),
358
256
  onMouseenter: composeEventHandlers([
359
- options.onMouseEnter,
257
+ eventHandlers.onMouseenter,
360
258
  enqueueIntentPreload,
361
259
  ]),
362
260
  onMouseover: composeEventHandlers([
363
- options.onMouseOver,
261
+ eventHandlers.onMouseover,
364
262
  enqueueIntentPreload,
365
263
  ]),
366
264
  onMouseleave: composeEventHandlers([
367
- options.onMouseLeave,
265
+ eventHandlers.onMouseleave,
368
266
  handleLeave,
369
267
  ]),
370
268
  onMouseout: composeEventHandlers([
371
- options.onMouseOut,
269
+ eventHandlers.onMouseout,
372
270
  handleLeave,
373
271
  ]),
374
272
  onTouchstart: composeEventHandlers([
375
- options.onTouchStart,
273
+ eventHandlers.onTouchstart,
376
274
  handleTouchStart,
377
275
  ]),
378
276
  };
379
277
  // Compute all props synchronously to avoid hydration mismatches
380
278
  // Using Vue.computed ensures props are calculated at render time, not after
381
279
  const computedProps = Vue.computed(() => {
382
- const result = {
383
- ...getPropsSafeToSpread(),
280
+ const { resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, } = resolvedStyleProps.value;
281
+ return combineResultProps({
384
282
  href: href.value,
283
+ options: options,
385
284
  ref,
386
- ...staticEventHandlers,
387
- disabled: !!options.disabled,
388
- target: options.target,
389
- };
390
- // Add style if present
391
- if (resolvedStyle.value) {
392
- result.style = resolvedStyle.value;
393
- }
394
- // Add class if present
395
- if (resolvedClassName.value) {
396
- result.class = resolvedClassName.value;
285
+ staticEventHandlers,
286
+ isActive: isActive.value,
287
+ isTransitioning: isTransitioning.value,
288
+ resolvedActiveProps,
289
+ resolvedInactiveProps,
290
+ resolvedClassName,
291
+ resolvedStyle,
292
+ });
293
+ });
294
+ // Return the computed ref itself - callers should access .value
295
+ return computedProps;
296
+ }
297
+ function resolveStyleProps({ options, isActive, }) {
298
+ const activeProps = options.activeProps || (() => ({ class: 'active' }));
299
+ const resolvedActiveProps = (isActive
300
+ ? typeof activeProps === 'function'
301
+ ? activeProps()
302
+ : activeProps
303
+ : {}) || { class: undefined, style: undefined };
304
+ const inactiveProps = options.inactiveProps || (() => ({}));
305
+ const resolvedInactiveProps = (isActive
306
+ ? {}
307
+ : typeof inactiveProps === 'function'
308
+ ? inactiveProps()
309
+ : inactiveProps) || { class: undefined, style: undefined };
310
+ const classes = [
311
+ options.class,
312
+ resolvedActiveProps?.class,
313
+ resolvedInactiveProps?.class,
314
+ ].filter(Boolean);
315
+ const resolvedClassName = classes.length ? classes.join(' ') : undefined;
316
+ const result = {};
317
+ // Merge styles from all sources
318
+ if (options.style) {
319
+ Object.assign(result, options.style);
320
+ }
321
+ if (resolvedActiveProps?.style) {
322
+ Object.assign(result, resolvedActiveProps.style);
323
+ }
324
+ if (resolvedInactiveProps?.style) {
325
+ Object.assign(result, resolvedInactiveProps.style);
326
+ }
327
+ const resolvedStyle = Object.keys(result).length > 0 ? result : undefined;
328
+ return {
329
+ resolvedActiveProps,
330
+ resolvedInactiveProps,
331
+ resolvedClassName,
332
+ resolvedStyle,
333
+ };
334
+ }
335
+ function combineResultProps({ href, options, isActive, isTransitioning, resolvedActiveProps, resolvedInactiveProps, resolvedClassName, resolvedStyle, ref, staticEventHandlers, }) {
336
+ const result = {
337
+ ...getPropsSafeToSpread(options),
338
+ ref,
339
+ ...staticEventHandlers,
340
+ href,
341
+ disabled: !!options.disabled,
342
+ target: options.target,
343
+ };
344
+ if (resolvedStyle) {
345
+ result.style = resolvedStyle;
346
+ }
347
+ if (resolvedClassName) {
348
+ result.class = resolvedClassName;
349
+ }
350
+ if (options.disabled) {
351
+ result.role = 'link';
352
+ result['aria-disabled'] = true;
353
+ }
354
+ if (isActive) {
355
+ result['data-status'] = 'active';
356
+ result['aria-current'] = 'page';
357
+ }
358
+ if (isTransitioning) {
359
+ result['data-transitioning'] = 'transitioning';
360
+ }
361
+ for (const key of Object.keys(resolvedActiveProps)) {
362
+ if (key !== 'class' && key !== 'style') {
363
+ result[key] = resolvedActiveProps[key];
397
364
  }
398
- // Add disabled props
399
- if (options.disabled) {
400
- result.role = 'link';
401
- result['aria-disabled'] = true;
365
+ }
366
+ for (const key of Object.keys(resolvedInactiveProps)) {
367
+ if (key !== 'class' && key !== 'style') {
368
+ result[key] = resolvedInactiveProps[key];
402
369
  }
403
- // Add active status
404
- if (isActive.value) {
405
- result['data-status'] = 'active';
406
- result['aria-current'] = 'page';
370
+ }
371
+ return result;
372
+ }
373
+ function getLinkEventHandlers(options) {
374
+ return {
375
+ onMouseenter: options.onMouseEnter ?? options.onMouseenter,
376
+ onMouseleave: options.onMouseLeave ?? options.onMouseleave,
377
+ onMouseover: options.onMouseOver ?? options.onMouseover,
378
+ onMouseout: options.onMouseOut ?? options.onMouseout,
379
+ onTouchstart: options.onTouchStart ?? options.onTouchstart,
380
+ };
381
+ }
382
+ const propsUnsafeToSpread = new Set([
383
+ 'activeProps',
384
+ 'inactiveProps',
385
+ 'activeOptions',
386
+ 'to',
387
+ 'preload',
388
+ 'preloadDelay',
389
+ 'hashScrollIntoView',
390
+ 'replace',
391
+ 'startTransition',
392
+ 'resetScroll',
393
+ 'viewTransition',
394
+ 'children',
395
+ 'target',
396
+ 'disabled',
397
+ 'style',
398
+ 'class',
399
+ 'onClick',
400
+ 'onBlur',
401
+ 'onFocus',
402
+ 'onMouseEnter',
403
+ 'onMouseenter',
404
+ 'onMouseLeave',
405
+ 'onMouseleave',
406
+ 'onMouseOver',
407
+ 'onMouseover',
408
+ 'onMouseOut',
409
+ 'onMouseout',
410
+ 'onTouchStart',
411
+ 'onTouchstart',
412
+ 'ignoreBlocker',
413
+ 'params',
414
+ 'search',
415
+ 'hash',
416
+ 'state',
417
+ 'mask',
418
+ 'reloadDocument',
419
+ '_asChild',
420
+ 'from',
421
+ 'additionalProps',
422
+ ]);
423
+ // Create safe props that can be spread
424
+ const getPropsSafeToSpread = (options) => {
425
+ const result = {};
426
+ for (const key in options) {
427
+ if (!propsUnsafeToSpread.has(key)) {
428
+ result[key] = options[key];
407
429
  }
408
- // Add transitioning status
409
- if (isTransitioning.value) {
410
- result['data-transitioning'] = 'transitioning';
430
+ }
431
+ return result;
432
+ };
433
+ function getIsActive({ activeOptions, loc, nextLoc, router, }) {
434
+ if (activeOptions?.exact) {
435
+ const testExact = exactPathTest(loc.pathname, nextLoc.pathname, router.basepath);
436
+ if (!testExact) {
437
+ return false;
411
438
  }
412
- // Merge active/inactive props (excluding class and style which are handled above)
413
- const activeP = resolvedActiveProps.value;
414
- const inactiveP = resolvedInactiveProps.value;
415
- for (const key of Object.keys(activeP)) {
416
- if (key !== 'class' && key !== 'style') {
417
- result[key] = activeP[key];
418
- }
439
+ }
440
+ else {
441
+ const currentPath = removeTrailingSlash(loc.pathname, router.basepath);
442
+ const nextPath = removeTrailingSlash(nextLoc.pathname, router.basepath);
443
+ const pathIsFuzzyEqual = currentPath.startsWith(nextPath) &&
444
+ (currentPath.length === nextPath.length ||
445
+ currentPath[nextPath.length] === '/');
446
+ if (!pathIsFuzzyEqual) {
447
+ return false;
419
448
  }
420
- for (const key of Object.keys(inactiveP)) {
421
- if (key !== 'class' && key !== 'style') {
422
- result[key] = inactiveP[key];
423
- }
449
+ }
450
+ if (activeOptions?.includeSearch ?? true) {
451
+ const searchTest = deepEqual(loc.search, nextLoc.search, {
452
+ partial: !activeOptions?.exact,
453
+ ignoreUndefined: !activeOptions?.explicitUndefined,
454
+ });
455
+ if (!searchTest) {
456
+ return false;
424
457
  }
425
- return result;
426
- });
427
- // Return the computed ref itself - callers should access .value
428
- return computedProps;
458
+ }
459
+ if (activeOptions?.includeHash) {
460
+ return loc.hash === nextLoc.hash;
461
+ }
462
+ return true;
463
+ }
464
+ function getHref({ options, router, nextLocation, }) {
465
+ if (options.disabled) {
466
+ return undefined;
467
+ }
468
+ const location = nextLocation?.maskedLocation ?? nextLocation;
469
+ // Use publicHref - it contains the correct href for display
470
+ // When a rewrite changes the origin, publicHref is the full URL
471
+ // Otherwise it's the origin-stripped path
472
+ // This avoids constructing URL objects in the hot path
473
+ const publicHref = location?.publicHref;
474
+ if (!publicHref)
475
+ return undefined;
476
+ const external = location?.external;
477
+ if (external)
478
+ return publicHref;
479
+ return router.history.createHref(publicHref) || '/';
429
480
  }
430
481
  export function createLink(Comp) {
431
482
  return Vue.defineComponent({
@@ -466,13 +517,11 @@ const LinkImpl = Vue.defineComponent({
466
517
  ],
467
518
  setup(props, { attrs, slots }) {
468
519
  // Call useLinkProps ONCE during setup with combined props and attrs
469
- // The returned object is a computed ref that updates reactively
470
520
  const allProps = { ...props, ...attrs };
471
- const linkPropsComputed = useLinkProps(allProps);
521
+ const linkPropsSource = useLinkProps(allProps);
472
522
  return () => {
473
523
  const Component = props._asChild || 'a';
474
- // Access the computed value to get fresh props each render
475
- const linkProps = linkPropsComputed.value;
524
+ const linkProps = Vue.unref(linkPropsSource);
476
525
  const isActive = linkProps['data-status'] === 'active';
477
526
  const isTransitioning = linkProps['data-transitioning'] === 'transitioning';
478
527
  // Create the slot content or empty array if no default slot