@tanstack/solid-router 1.167.4 → 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 (177) hide show
  1. package/dist/cjs/Match.cjs +244 -223
  2. package/dist/cjs/Match.cjs.map +1 -1
  3. package/dist/cjs/Match.d.cts +1 -3
  4. package/dist/cjs/Matches.cjs +32 -31
  5. package/dist/cjs/Matches.cjs.map +1 -1
  6. package/dist/cjs/Scripts.cjs +10 -8
  7. package/dist/cjs/Scripts.cjs.map +1 -1
  8. package/dist/cjs/Scripts.d.cts +2 -1
  9. package/dist/cjs/Transitioner.cjs +26 -26
  10. package/dist/cjs/Transitioner.cjs.map +1 -1
  11. package/dist/cjs/headContentUtils.cjs +15 -15
  12. package/dist/cjs/headContentUtils.cjs.map +1 -1
  13. package/dist/cjs/index.cjs +1 -1
  14. package/dist/cjs/index.dev.cjs +1 -1
  15. package/dist/cjs/link.cjs +119 -84
  16. package/dist/cjs/link.cjs.map +1 -1
  17. package/dist/cjs/matchContext.cjs +7 -5
  18. package/dist/cjs/matchContext.cjs.map +1 -1
  19. package/dist/cjs/matchContext.d.cts +8 -2
  20. package/dist/cjs/not-found.cjs +8 -4
  21. package/dist/cjs/not-found.cjs.map +1 -1
  22. package/dist/cjs/not-found.d.cts +1 -1
  23. package/dist/cjs/router.cjs +2 -1
  24. package/dist/cjs/router.cjs.map +1 -1
  25. package/dist/cjs/routerStores.cjs +67 -0
  26. package/dist/cjs/routerStores.cjs.map +1 -0
  27. package/dist/cjs/routerStores.d.cts +10 -0
  28. package/dist/cjs/ssr/RouterClient.cjs +1 -1
  29. package/dist/cjs/ssr/RouterClient.cjs.map +1 -1
  30. package/dist/cjs/ssr/renderRouterToStream.cjs +1 -1
  31. package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -1
  32. package/dist/cjs/ssr/renderRouterToString.cjs +2 -2
  33. package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -1
  34. package/dist/cjs/ssr/renderRouterToString.d.cts +1 -1
  35. package/dist/cjs/useCanGoBack.cjs +6 -2
  36. package/dist/cjs/useCanGoBack.cjs.map +1 -1
  37. package/dist/cjs/useCanGoBack.d.cts +2 -1
  38. package/dist/cjs/useLoaderDeps.cjs +2 -3
  39. package/dist/cjs/useLoaderDeps.cjs.map +1 -1
  40. package/dist/cjs/useLocation.cjs +13 -2
  41. package/dist/cjs/useLocation.cjs.map +1 -1
  42. package/dist/cjs/useMatch.cjs +17 -15
  43. package/dist/cjs/useMatch.cjs.map +1 -1
  44. package/dist/cjs/useParams.cjs +1 -1
  45. package/dist/cjs/useParams.cjs.map +1 -1
  46. package/dist/cjs/useRouterState.cjs +12 -19
  47. package/dist/cjs/useRouterState.cjs.map +1 -1
  48. package/dist/cjs/useSearch.cjs +2 -1
  49. package/dist/cjs/useSearch.cjs.map +1 -1
  50. package/dist/cjs/utils.cjs +0 -14
  51. package/dist/cjs/utils.cjs.map +1 -1
  52. package/dist/cjs/utils.d.cts +0 -4
  53. package/dist/esm/Match.d.ts +1 -3
  54. package/dist/esm/Match.js +245 -224
  55. package/dist/esm/Match.js.map +1 -1
  56. package/dist/esm/Matches.js +34 -33
  57. package/dist/esm/Matches.js.map +1 -1
  58. package/dist/esm/Scripts.d.ts +2 -1
  59. package/dist/esm/Scripts.js +8 -7
  60. package/dist/esm/Scripts.js.map +1 -1
  61. package/dist/esm/Transitioner.js +26 -26
  62. package/dist/esm/Transitioner.js.map +1 -1
  63. package/dist/esm/headContentUtils.js +15 -15
  64. package/dist/esm/headContentUtils.js.map +1 -1
  65. package/dist/esm/index.dev.js +1 -1
  66. package/dist/esm/index.js +1 -1
  67. package/dist/esm/link.js +120 -85
  68. package/dist/esm/link.js.map +1 -1
  69. package/dist/esm/matchContext.d.ts +8 -2
  70. package/dist/esm/matchContext.js +7 -4
  71. package/dist/esm/matchContext.js.map +1 -1
  72. package/dist/esm/not-found.d.ts +1 -1
  73. package/dist/esm/not-found.js +6 -3
  74. package/dist/esm/not-found.js.map +1 -1
  75. package/dist/esm/router.js +2 -1
  76. package/dist/esm/router.js.map +1 -1
  77. package/dist/esm/routerStores.d.ts +10 -0
  78. package/dist/esm/routerStores.js +65 -0
  79. package/dist/esm/routerStores.js.map +1 -0
  80. package/dist/esm/ssr/RouterClient.js +1 -1
  81. package/dist/esm/ssr/RouterClient.js.map +1 -1
  82. package/dist/esm/ssr/renderRouterToStream.js +1 -1
  83. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  84. package/dist/esm/ssr/renderRouterToString.d.ts +1 -1
  85. package/dist/esm/ssr/renderRouterToString.js +2 -2
  86. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  87. package/dist/esm/useCanGoBack.d.ts +2 -1
  88. package/dist/esm/useCanGoBack.js +4 -2
  89. package/dist/esm/useCanGoBack.js.map +1 -1
  90. package/dist/esm/useLoaderDeps.js +2 -3
  91. package/dist/esm/useLoaderDeps.js.map +1 -1
  92. package/dist/esm/useLocation.js +11 -2
  93. package/dist/esm/useLocation.js.map +1 -1
  94. package/dist/esm/useMatch.js +18 -16
  95. package/dist/esm/useMatch.js.map +1 -1
  96. package/dist/esm/useParams.js +1 -1
  97. package/dist/esm/useParams.js.map +1 -1
  98. package/dist/esm/useRouterState.js +10 -18
  99. package/dist/esm/useRouterState.js.map +1 -1
  100. package/dist/esm/useSearch.js +2 -1
  101. package/dist/esm/useSearch.js.map +1 -1
  102. package/dist/esm/utils.d.ts +0 -4
  103. package/dist/esm/utils.js +1 -14
  104. package/dist/esm/utils.js.map +1 -1
  105. package/dist/source/Match.d.ts +1 -3
  106. package/dist/source/Match.jsx +246 -237
  107. package/dist/source/Match.jsx.map +1 -1
  108. package/dist/source/Matches.jsx +42 -44
  109. package/dist/source/Matches.jsx.map +1 -1
  110. package/dist/source/Scripts.d.ts +2 -1
  111. package/dist/source/Scripts.jsx +31 -36
  112. package/dist/source/Scripts.jsx.map +1 -1
  113. package/dist/source/Transitioner.jsx +26 -31
  114. package/dist/source/Transitioner.jsx.map +1 -1
  115. package/dist/source/headContentUtils.jsx +64 -72
  116. package/dist/source/headContentUtils.jsx.map +1 -1
  117. package/dist/source/link.jsx +136 -107
  118. package/dist/source/link.jsx.map +1 -1
  119. package/dist/source/matchContext.d.ts +8 -2
  120. package/dist/source/matchContext.jsx +7 -3
  121. package/dist/source/matchContext.jsx.map +1 -1
  122. package/dist/source/not-found.d.ts +1 -1
  123. package/dist/source/not-found.jsx +6 -5
  124. package/dist/source/not-found.jsx.map +1 -1
  125. package/dist/source/router.js +2 -1
  126. package/dist/source/router.js.map +1 -1
  127. package/dist/source/routerStores.d.ts +10 -0
  128. package/dist/source/routerStores.js +71 -0
  129. package/dist/source/routerStores.js.map +1 -0
  130. package/dist/source/ssr/RouterClient.jsx +1 -1
  131. package/dist/source/ssr/RouterClient.jsx.map +1 -1
  132. package/dist/source/ssr/renderRouterToStream.jsx +1 -1
  133. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -1
  134. package/dist/source/ssr/renderRouterToString.d.ts +1 -1
  135. package/dist/source/ssr/renderRouterToString.jsx +2 -2
  136. package/dist/source/ssr/renderRouterToString.jsx.map +1 -1
  137. package/dist/source/useCanGoBack.d.ts +2 -1
  138. package/dist/source/useCanGoBack.js +4 -2
  139. package/dist/source/useCanGoBack.js.map +1 -1
  140. package/dist/source/useLoaderDeps.jsx +2 -3
  141. package/dist/source/useLoaderDeps.jsx.map +1 -1
  142. package/dist/source/useLocation.jsx +13 -3
  143. package/dist/source/useLocation.jsx.map +1 -1
  144. package/dist/source/useMatch.jsx +30 -27
  145. package/dist/source/useMatch.jsx.map +1 -1
  146. package/dist/source/useParams.jsx +1 -1
  147. package/dist/source/useParams.jsx.map +1 -1
  148. package/dist/source/useRouterState.jsx +12 -33
  149. package/dist/source/useRouterState.jsx.map +1 -1
  150. package/dist/source/useSearch.jsx +2 -1
  151. package/dist/source/useSearch.jsx.map +1 -1
  152. package/dist/source/utils.d.ts +0 -4
  153. package/dist/source/utils.js +0 -13
  154. package/dist/source/utils.js.map +1 -1
  155. package/package.json +2 -3
  156. package/skills/solid-router/SKILL.md +2 -0
  157. package/src/Match.tsx +351 -304
  158. package/src/Matches.tsx +49 -52
  159. package/src/Scripts.tsx +40 -41
  160. package/src/Transitioner.tsx +67 -66
  161. package/src/headContentUtils.tsx +89 -91
  162. package/src/link.tsx +179 -141
  163. package/src/matchContext.tsx +16 -7
  164. package/src/not-found.tsx +6 -6
  165. package/src/router.ts +2 -1
  166. package/src/routerStores.ts +107 -0
  167. package/src/ssr/RouterClient.tsx +1 -1
  168. package/src/ssr/renderRouterToStream.tsx +1 -1
  169. package/src/ssr/renderRouterToString.tsx +2 -2
  170. package/src/useCanGoBack.ts +6 -2
  171. package/src/useLoaderDeps.tsx +2 -3
  172. package/src/useLocation.tsx +18 -5
  173. package/src/useMatch.tsx +36 -43
  174. package/src/useParams.tsx +2 -3
  175. package/src/useRouterState.tsx +17 -41
  176. package/src/useSearch.tsx +2 -1
  177. package/src/utils.ts +0 -20
package/src/link.tsx CHANGED
@@ -13,7 +13,6 @@ import {
13
13
 
14
14
  import { isServer } from '@tanstack/router-core/isServer'
15
15
  import { Dynamic } from 'solid-js/web'
16
- import { useRouterState } from './useRouterState'
17
16
  import { useRouter } from './useRouter'
18
17
 
19
18
  import { useIntersectionObserver } from './utils'
@@ -52,8 +51,8 @@ export function useLinkProps<
52
51
  const [local, rest] = Solid.splitProps(
53
52
  Solid.mergeProps(
54
53
  {
55
- activeProps: () => ({ class: 'active' }),
56
- inactiveProps: () => ({}),
54
+ activeProps: STATIC_ACTIVE_PROPS_GET,
55
+ inactiveProps: STATIC_INACTIVE_PROPS_GET,
57
56
  },
58
57
  options,
59
58
  ),
@@ -123,33 +122,20 @@ export function useLinkProps<
123
122
  'unsafeRelative',
124
123
  ])
125
124
 
126
- const currentLocation = useRouterState({
127
- select: (s) => s.location,
128
- })
129
-
130
- const buildLocationKey = useRouterState({
131
- select: (s) => {
132
- const leaf = s.matches[s.matches.length - 1]
133
- return {
134
- search: leaf?.search,
135
- hash: s.location.hash,
136
- path: leaf?.pathname, // path + params
137
- }
138
- },
139
- })
125
+ const currentLocation = Solid.createMemo(
126
+ () => router.stores.location.state,
127
+ undefined,
128
+ { equals: (prev, next) => prev.href === next.href },
129
+ )
140
130
 
141
- const from = options.from
142
-
143
- const _options = () => {
144
- return {
145
- ...options,
146
- from,
147
- }
148
- }
131
+ const _options = () => options
149
132
 
150
133
  const next = Solid.createMemo(() => {
151
- buildLocationKey()
152
- return router.buildLocation(_options() as any)
134
+ // Rebuild when inherited search/hash or the current route context changes.
135
+ const _fromLocation = currentLocation()
136
+ const options = { _fromLocation, ..._options() } as any
137
+ // untrack because router-core will also access stores, which are signals in solid
138
+ return Solid.untrack(() => router.buildLocation(options))
153
139
  })
154
140
 
155
141
  const hrefOption = Solid.createMemo(() => {
@@ -185,15 +171,13 @@ export function useLinkProps<
185
171
  return _href.href
186
172
  }
187
173
  const to = _options().to
188
- const isSafeInternal =
189
- typeof to === 'string' &&
190
- to.charCodeAt(0) === 47 && // '/'
191
- to.charCodeAt(1) !== 47 // but not '//'
192
- if (isSafeInternal) return undefined
174
+ const safeInternal = isSafeInternal(to)
175
+ if (safeInternal) return undefined
176
+ if (typeof to !== 'string' || to.indexOf(':') === -1) return undefined
193
177
  try {
194
178
  new URL(to as any)
195
179
  // Block dangerous protocols like javascript:, blob:, data:
196
- if (isDangerousProtocol(to as string, router.protocolAllowlist)) {
180
+ if (isDangerousProtocol(to, router.protocolAllowlist)) {
197
181
  if (process.env.NODE_ENV !== 'production') {
198
182
  console.warn(`Blocked Link with dangerous protocol: ${to}`)
199
183
  }
@@ -215,56 +199,60 @@ export function useLinkProps<
215
199
 
216
200
  const isActive = Solid.createMemo(() => {
217
201
  if (externalLink()) return false
218
- if (local.activeOptions?.exact) {
202
+ const activeOptions = local.activeOptions
203
+ const current = currentLocation()
204
+ const nextLocation = next()
205
+
206
+ if (activeOptions?.exact) {
219
207
  const testExact = exactPathTest(
220
- currentLocation().pathname,
221
- next().pathname,
208
+ current.pathname,
209
+ nextLocation.pathname,
222
210
  router.basepath,
223
211
  )
224
212
  if (!testExact) {
225
213
  return false
226
214
  }
227
215
  } else {
228
- const currentPathSplit = removeTrailingSlash(
229
- currentLocation().pathname,
230
- router.basepath,
231
- ).split('/')
232
- const nextPathSplit = removeTrailingSlash(
233
- next()?.pathname,
216
+ const currentPath = removeTrailingSlash(current.pathname, router.basepath)
217
+ const nextPath = removeTrailingSlash(
218
+ nextLocation.pathname,
234
219
  router.basepath,
235
- )?.split('/')
236
-
237
- const pathIsFuzzyEqual = nextPathSplit?.every(
238
- (d, i) => d === currentPathSplit[i],
239
220
  )
221
+
222
+ const pathIsFuzzyEqual =
223
+ currentPath.startsWith(nextPath) &&
224
+ (currentPath.length === nextPath.length ||
225
+ currentPath[nextPath.length] === '/')
240
226
  if (!pathIsFuzzyEqual) {
241
227
  return false
242
228
  }
243
229
  }
244
230
 
245
- if (local.activeOptions?.includeSearch ?? true) {
246
- const searchTest = deepEqual(currentLocation().search, next().search, {
247
- partial: !local.activeOptions?.exact,
248
- ignoreUndefined: !local.activeOptions?.explicitUndefined,
231
+ if (activeOptions?.includeSearch ?? true) {
232
+ const searchTest = deepEqual(current.search, nextLocation.search, {
233
+ partial: !activeOptions?.exact,
234
+ ignoreUndefined: !activeOptions?.explicitUndefined,
249
235
  })
250
236
  if (!searchTest) {
251
237
  return false
252
238
  }
253
239
  }
254
240
 
255
- if (local.activeOptions?.includeHash) {
241
+ if (activeOptions?.includeHash) {
256
242
  const currentHash =
257
- shouldHydrateHash && !hasHydrated() ? '' : currentLocation().hash
258
- return currentHash === next().hash
243
+ shouldHydrateHash && !hasHydrated() ? '' : current.hash
244
+ return currentHash === nextLocation.hash
259
245
  }
260
246
  return true
261
247
  })
262
248
 
263
249
  const doPreload = () =>
264
- router.preloadRoute(_options() as any).catch((err: any) => {
265
- console.warn(err)
266
- console.warn(preloadWarning)
267
- })
250
+ router
251
+ .preloadRoute({ ..._options(), _builtLocation: next() } as any)
252
+ .catch((err: any) => {
253
+ console.warn(err)
254
+ console.warn(preloadWarning)
255
+ })
268
256
 
269
257
  const preloadViewportIoCallback = (
270
258
  entry: IntersectionObserverEntry | undefined,
@@ -393,100 +381,139 @@ export function useLinkProps<
393
381
  }
394
382
  }
395
383
 
396
- /** Call a JSX.EventHandlerUnion with the event. */
397
- function callHandler<T, TEvent extends Event>(
398
- event: TEvent & { currentTarget: T; target: Element },
399
- handler: Solid.JSX.EventHandlerUnion<T, TEvent> | undefined,
400
- ) {
401
- if (handler) {
402
- if (typeof handler === 'function') {
403
- handler(event)
404
- } else {
405
- handler[0](handler[1], event)
406
- }
407
- }
384
+ const simpleStyling = Solid.createMemo(
385
+ () =>
386
+ local.activeProps === STATIC_ACTIVE_PROPS_GET &&
387
+ local.inactiveProps === STATIC_INACTIVE_PROPS_GET &&
388
+ local.class === undefined &&
389
+ local.style === undefined,
390
+ )
391
+
392
+ const onClick = createComposedHandler(() => local.onClick, handleClick)
393
+ const onBlur = createComposedHandler(() => local.onBlur, handleLeave)
394
+ const onFocus = createComposedHandler(
395
+ () => local.onFocus,
396
+ enqueueIntentPreload,
397
+ )
398
+ const onMouseEnter = createComposedHandler(
399
+ () => local.onMouseEnter,
400
+ enqueueIntentPreload,
401
+ )
402
+ const onMouseOver = createComposedHandler(
403
+ () => local.onMouseOver,
404
+ enqueueIntentPreload,
405
+ )
406
+ const onMouseLeave = createComposedHandler(
407
+ () => local.onMouseLeave,
408
+ handleLeave,
409
+ )
410
+ const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave)
411
+ const onTouchStart = createComposedHandler(
412
+ () => local.onTouchStart,
413
+ handleTouchStart,
414
+ )
408
415
 
409
- return event.defaultPrevented
416
+ type ResolvedLinkStateProps = Omit<Solid.ComponentProps<'a'>, 'style'> & {
417
+ style?: Solid.JSX.CSSProperties
410
418
  }
411
419
 
412
- function composeEventHandlers<T>(
413
- handlers: Array<Solid.JSX.EventHandlerUnion<T, any> | undefined>,
414
- ) {
415
- return (event: any) => {
416
- for (const handler of handlers) {
417
- callHandler(event, handler)
420
+ const resolvedProps = Solid.createMemo(() => {
421
+ const active = isActive()
422
+
423
+ const base = {
424
+ href: hrefOption()?.href,
425
+ ref: mergeRefs(setRef, _options().ref),
426
+ onClick,
427
+ onBlur,
428
+ onFocus,
429
+ onMouseEnter,
430
+ onMouseOver,
431
+ onMouseLeave,
432
+ onMouseOut,
433
+ onTouchStart,
434
+ disabled: !!local.disabled,
435
+ target: local.target,
436
+ ...(local.disabled && STATIC_DISABLED_PROPS),
437
+ ...(isTransitioning() && STATIC_TRANSITIONING_ATTRIBUTES),
438
+ }
439
+
440
+ if (simpleStyling()) {
441
+ return {
442
+ ...base,
443
+ ...(active && STATIC_DEFAULT_ACTIVE_ATTRIBUTES),
418
444
  }
419
445
  }
420
- }
421
446
 
422
- // Get the active props
423
- const resolvedActiveProps: () => Omit<Solid.ComponentProps<'a'>, 'style'> & {
424
- style?: Solid.JSX.CSSProperties
425
- } = () =>
426
- isActive() ? (functionalUpdate(local.activeProps as any, {}) ?? {}) : {}
427
-
428
- // Get the inactive props
429
- const resolvedInactiveProps: () => Omit<
430
- Solid.ComponentProps<'a'>,
431
- 'style'
432
- > & { style?: Solid.JSX.CSSProperties } = () =>
433
- isActive() ? {} : functionalUpdate(local.inactiveProps, {})
434
-
435
- const resolvedClassName = () =>
436
- [local.class, resolvedActiveProps().class, resolvedInactiveProps().class]
447
+ const activeProps: ResolvedLinkStateProps = active
448
+ ? (functionalUpdate(local.activeProps as any, {}) ?? EMPTY_OBJECT)
449
+ : EMPTY_OBJECT
450
+ const inactiveProps: ResolvedLinkStateProps = active
451
+ ? EMPTY_OBJECT
452
+ : functionalUpdate(local.inactiveProps, {})
453
+ const style = {
454
+ ...local.style,
455
+ ...activeProps.style,
456
+ ...inactiveProps.style,
457
+ }
458
+ const className = [local.class, activeProps.class, inactiveProps.class]
437
459
  .filter(Boolean)
438
460
  .join(' ')
439
461
 
440
- const resolvedStyle = () => ({
441
- ...local.style,
442
- ...resolvedActiveProps().style,
443
- ...resolvedInactiveProps().style,
462
+ return {
463
+ ...activeProps,
464
+ ...inactiveProps,
465
+ ...base,
466
+ ...(Object.keys(style).length ? { style } : undefined),
467
+ ...(className ? { class: className } : undefined),
468
+ ...(active && STATIC_ACTIVE_ATTRIBUTES),
469
+ } as ResolvedLinkStateProps
444
470
  })
445
471
 
446
- return Solid.mergeProps(
447
- propsSafeToSpread,
448
- resolvedActiveProps,
449
- resolvedInactiveProps,
450
- () => {
451
- return {
452
- href: hrefOption()?.href,
453
- ref: mergeRefs(setRef, _options().ref),
454
- onClick: composeEventHandlers([local.onClick, handleClick]),
455
- onBlur: composeEventHandlers([local.onBlur, handleLeave]),
456
- onFocus: composeEventHandlers([local.onFocus, enqueueIntentPreload]),
457
- onMouseEnter: composeEventHandlers([
458
- local.onMouseEnter,
459
- enqueueIntentPreload,
460
- ]),
461
- onMouseOver: composeEventHandlers([
462
- local.onMouseOver,
463
- enqueueIntentPreload,
464
- ]),
465
- onMouseLeave: composeEventHandlers([local.onMouseLeave, handleLeave]),
466
- onMouseOut: composeEventHandlers([local.onMouseOut, handleLeave]),
467
- onTouchStart: composeEventHandlers([
468
- local.onTouchStart,
469
- handleTouchStart,
470
- ]),
471
- disabled: !!local.disabled,
472
- target: local.target,
473
- ...(() => {
474
- const s = resolvedStyle()
475
- return Object.keys(s).length ? { style: s } : {}
476
- })(),
477
- ...(() => {
478
- const c = resolvedClassName()
479
- return c ? { class: c } : {}
480
- })(),
481
- ...(local.disabled && {
482
- role: 'link',
483
- 'aria-disabled': true,
484
- }),
485
- ...(isActive() && { 'data-status': 'active', 'aria-current': 'page' }),
486
- ...(isTransitioning() && { 'data-transitioning': 'transitioning' }),
487
- }
488
- },
489
- ) as any
472
+ return Solid.mergeProps(propsSafeToSpread, resolvedProps) as any
473
+ }
474
+
475
+ const STATIC_ACTIVE_PROPS = { class: 'active' }
476
+ const STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS
477
+ const EMPTY_OBJECT = {}
478
+ const STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT
479
+ const STATIC_DEFAULT_ACTIVE_ATTRIBUTES = {
480
+ class: 'active',
481
+ 'data-status': 'active',
482
+ 'aria-current': 'page',
483
+ }
484
+ const STATIC_DISABLED_PROPS = {
485
+ role: 'link',
486
+ 'aria-disabled': true,
487
+ }
488
+ const STATIC_ACTIVE_ATTRIBUTES = {
489
+ 'data-status': 'active',
490
+ 'aria-current': 'page',
491
+ }
492
+ const STATIC_TRANSITIONING_ATTRIBUTES = {
493
+ 'data-transitioning': 'transitioning',
494
+ }
495
+
496
+ /** Call a JSX.EventHandlerUnion with the event. */
497
+ function callHandler<T, TEvent extends Event>(
498
+ event: TEvent & { currentTarget: T; target: Element },
499
+ handler: Solid.JSX.EventHandlerUnion<T, TEvent>,
500
+ ) {
501
+ if (typeof handler === 'function') {
502
+ handler(event)
503
+ } else {
504
+ handler[0](handler[1], event)
505
+ }
506
+ return event.defaultPrevented
507
+ }
508
+
509
+ function createComposedHandler<T, TEvent extends Event>(
510
+ getHandler: () => Solid.JSX.EventHandlerUnion<T, TEvent> | undefined,
511
+ fallback: (event: TEvent) => void,
512
+ ) {
513
+ return (event: TEvent & { currentTarget: T; target: Element }) => {
514
+ const handler = getHandler()
515
+ if (!handler || !callHandler(event, handler)) fallback(event)
516
+ }
490
517
  }
491
518
 
492
519
  export type UseLinkPropsOptions<
@@ -645,8 +672,12 @@ export const Link: LinkComponent<'a'> = (props) => {
645
672
  )
646
673
  }
647
674
 
675
+ if (!local._asChild) {
676
+ return <a {...linkProps}>{children()}</a>
677
+ }
678
+
648
679
  return (
649
- <Dynamic component={local._asChild ? local._asChild : 'a'} {...linkProps}>
680
+ <Dynamic component={local._asChild as Solid.ValidComponent} {...linkProps}>
650
681
  {children()}
651
682
  </Dynamic>
652
683
  )
@@ -656,6 +687,13 @@ function isCtrlEvent(e: MouseEvent) {
656
687
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
657
688
  }
658
689
 
690
+ function isSafeInternal(to: unknown) {
691
+ if (typeof to !== 'string') return false
692
+ const zero = to.charCodeAt(0)
693
+ if (zero === 47) return to.charCodeAt(1) !== 47 // '/' but not '//'
694
+ return zero === 46 // '.', '..', './', '../'
695
+ }
696
+
659
697
  export type LinkOptionsFnOptions<
660
698
  TOptions,
661
699
  TComp,
@@ -1,10 +1,19 @@
1
1
  import * as Solid from 'solid-js'
2
+ import type { AnyRouteMatch } from '@tanstack/router-core'
2
3
 
3
- export const matchContext = Solid.createContext<
4
- Solid.Accessor<string | undefined>
5
- >(() => undefined)
4
+ export type NearestMatchContextValue = {
5
+ matchId: Solid.Accessor<string | undefined>
6
+ routeId: Solid.Accessor<string | undefined>
7
+ match: Solid.Accessor<AnyRouteMatch | undefined>
8
+ hasPending: Solid.Accessor<boolean>
9
+ }
6
10
 
7
- // N.B. this only exists so we can conditionally call useContext on it when we are not interested in the nearest match
8
- export const dummyMatchContext = Solid.createContext<
9
- Solid.Accessor<string | undefined>
10
- >(() => undefined)
11
+ const defaultNearestMatchContext: NearestMatchContextValue = {
12
+ matchId: () => undefined,
13
+ routeId: () => undefined,
14
+ match: () => undefined,
15
+ hasPending: () => false,
16
+ }
17
+
18
+ export const nearestMatchContext =
19
+ Solid.createContext<NearestMatchContextValue>(defaultNearestMatchContext)
package/src/not-found.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isNotFound } from '@tanstack/router-core'
2
+ import * as Solid from 'solid-js'
2
3
  import { CatchBoundary } from './CatchBoundary'
3
- import { useRouterState } from './useRouterState'
4
- import type * as Solid from 'solid-js'
4
+ import { useRouter } from './useRouter'
5
5
  import type { NotFoundError } from '@tanstack/router-core'
6
6
 
7
7
  export function CatchNotFound(props: {
@@ -9,14 +9,14 @@ export function CatchNotFound(props: {
9
9
  onCatch?: (error: Error) => void
10
10
  children: Solid.JSX.Element
11
11
  }) {
12
+ const router = useRouter()
12
13
  // TODO: Some way for the user to programmatically reset the not-found boundary?
13
- const resetKey = useRouterState({
14
- select: (s) => `not-found-${s.location.pathname}-${s.status}`,
15
- })
14
+ const pathname = Solid.createMemo(() => router.stores.location.state.pathname)
15
+ const status = Solid.createMemo(() => router.stores.status.state)
16
16
 
17
17
  return (
18
18
  <CatchBoundary
19
- getResetKey={() => resetKey()}
19
+ getResetKey={() => `not-found-${pathname()}-${status()}`}
20
20
  onCatch={(error) => {
21
21
  if (isNotFound(error)) {
22
22
  props.onCatch?.(error)
package/src/router.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { RouterCore } from '@tanstack/router-core'
2
2
  import { createFileRoute, createLazyFileRoute } from './fileRoute'
3
+ import { getStoreFactory } from './routerStores'
3
4
  import type { RouterHistory } from '@tanstack/history'
4
5
  import type {
5
6
  AnyRoute,
@@ -99,7 +100,7 @@ export class Router<
99
100
  TDehydrated
100
101
  >,
101
102
  ) {
102
- super(options)
103
+ super(options, getStoreFactory)
103
104
  }
104
105
  }
105
106
 
@@ -0,0 +1,107 @@
1
+ import * as Solid from 'solid-js'
2
+ import {
3
+ createNonReactiveMutableStore,
4
+ createNonReactiveReadonlyStore,
5
+ } from '@tanstack/router-core'
6
+ import { isServer } from '@tanstack/router-core/isServer'
7
+ import type {
8
+ AnyRoute,
9
+ GetStoreConfig,
10
+ RouterReadableStore,
11
+ RouterStores,
12
+ RouterWritableStore,
13
+ } from '@tanstack/router-core'
14
+
15
+ declare module '@tanstack/router-core' {
16
+ export interface RouterStores<in out TRouteTree extends AnyRoute> {
17
+ /** Maps each active routeId to the matchId of its child in the match tree. */
18
+ childMatchIdByRouteId: RouterReadableStore<Record<string, string>>
19
+ /** Maps each pending routeId to true for quick lookup. */
20
+ pendingRouteIds: RouterReadableStore<Record<string, boolean>>
21
+ }
22
+ }
23
+
24
+ function initRouterStores(
25
+ stores: RouterStores<AnyRoute>,
26
+ createReadonlyStore: <TValue>(
27
+ read: () => TValue,
28
+ ) => RouterReadableStore<TValue>,
29
+ ) {
30
+ stores.childMatchIdByRouteId = createReadonlyStore(() => {
31
+ const ids = stores.matchesId.state
32
+ const obj: Record<string, string> = {}
33
+ for (let i = 0; i < ids.length - 1; i++) {
34
+ const parentStore = stores.activeMatchStoresById.get(ids[i]!)
35
+ if (parentStore?.routeId) {
36
+ obj[parentStore.routeId] = ids[i + 1]!
37
+ }
38
+ }
39
+ return obj
40
+ })
41
+
42
+ stores.pendingRouteIds = createReadonlyStore(() => {
43
+ const ids = stores.pendingMatchesId.state
44
+ const obj: Record<string, boolean> = {}
45
+ for (const id of ids) {
46
+ const store = stores.pendingMatchStoresById.get(id)
47
+ if (store?.routeId) {
48
+ obj[store.routeId] = true
49
+ }
50
+ }
51
+ return obj
52
+ })
53
+ }
54
+
55
+ function createSolidMutableStore<TValue>(
56
+ initialValue: TValue,
57
+ ): RouterWritableStore<TValue> {
58
+ const [signal, setSignal] = Solid.createSignal(initialValue)
59
+
60
+ return {
61
+ get state() {
62
+ return signal()
63
+ },
64
+ setState: setSignal,
65
+ }
66
+ }
67
+
68
+ let finalizationRegistry: FinalizationRegistry<() => void> | null = null
69
+ if (typeof globalThis !== 'undefined' && 'FinalizationRegistry' in globalThis) {
70
+ finalizationRegistry = new FinalizationRegistry((cb) => cb())
71
+ }
72
+
73
+ function createSolidReadonlyStore<TValue>(
74
+ read: () => TValue,
75
+ ): RouterReadableStore<TValue> {
76
+ let dispose!: () => void
77
+ const memo = Solid.createRoot((d) => {
78
+ dispose = d
79
+ return Solid.createMemo(read)
80
+ })
81
+ const store = {
82
+ get state() {
83
+ return memo()
84
+ },
85
+ }
86
+ finalizationRegistry?.register(store, dispose)
87
+ return store
88
+ }
89
+
90
+ export const getStoreFactory: GetStoreConfig = (opts) => {
91
+ if (isServer ?? opts.isServer) {
92
+ return {
93
+ createMutableStore: createNonReactiveMutableStore,
94
+ createReadonlyStore: createNonReactiveReadonlyStore,
95
+ batch: (fn) => fn(),
96
+ init: (stores) =>
97
+ initRouterStores(stores, createNonReactiveReadonlyStore),
98
+ }
99
+ }
100
+
101
+ return {
102
+ createMutableStore: createSolidMutableStore,
103
+ createReadonlyStore: createSolidReadonlyStore,
104
+ batch: Solid.batch,
105
+ init: (stores) => initRouterStores(stores, createSolidReadonlyStore),
106
+ }
107
+ }
@@ -11,7 +11,7 @@ const Dummy = (props: { children?: JSXElement }) => <>{props.children}</>
11
11
 
12
12
  export function RouterClient(props: { router: AnyRouter }) {
13
13
  if (!hydrationPromise) {
14
- if (!props.router.state.matches.length) {
14
+ if (!props.router.stores.matchesId.state.length) {
15
15
  hydrationPromise = hydrate(props.router)
16
16
  } else {
17
17
  hydrationPromise = Promise.resolve()
@@ -52,7 +52,7 @@ export const renderRouterToStream = async ({
52
52
  readable as unknown as ReadableStream,
53
53
  )
54
54
  return new Response(responseStream as any, {
55
- status: router.state.statusCode,
55
+ status: router.stores.statusCode.state,
56
56
  headers: responseHeaders,
57
57
  })
58
58
  }
@@ -3,7 +3,7 @@ import { makeSsrSerovalPlugin } from '@tanstack/router-core'
3
3
  import type { AnyRouter } from '@tanstack/router-core'
4
4
  import type { JSXElement } from 'solid-js'
5
5
 
6
- export const renderRouterToString = async ({
6
+ export const renderRouterToString = ({
7
7
  router,
8
8
  responseHeaders,
9
9
  children,
@@ -32,7 +32,7 @@ export const renderRouterToString = async ({
32
32
  html = html.replace(`</body>`, () => `${injectedHtml}</body>`)
33
33
  }
34
34
  return new Response(`<!DOCTYPE html>${html}`, {
35
- status: router.state.statusCode,
35
+ status: router.stores.statusCode.state,
36
36
  headers: responseHeaders,
37
37
  })
38
38
  } catch (error) {
@@ -1,5 +1,9 @@
1
- import { useRouterState } from './useRouterState'
1
+ import * as Solid from 'solid-js'
2
+ import { useRouter } from './useRouter'
2
3
 
3
4
  export function useCanGoBack() {
4
- return useRouterState({ select: (s) => s.location.state.__TSR_index !== 0 })
5
+ const router = useRouter()
6
+ return Solid.createMemo(
7
+ () => router.stores.location.state.state.__TSR_index !== 0,
8
+ )
5
9
  }
@@ -37,11 +37,10 @@ export function useLoaderDeps<
37
37
  >(
38
38
  opts: UseLoaderDepsOptions<TRouter, TFrom, TSelected>,
39
39
  ): Accessor<UseLoaderDepsResult<TRouter, TFrom, TSelected>> {
40
- const { select, ...rest } = opts
41
40
  return useMatch({
42
- ...rest,
41
+ ...opts,
43
42
  select: (s) => {
44
- return select ? select(s.loaderDeps) : s.loaderDeps
43
+ return opts.select ? opts.select(s.loaderDeps) : s.loaderDeps
45
44
  },
46
45
  }) as Accessor<UseLoaderDepsResult<TRouter, TFrom, TSelected>>
47
46
  }