@tanstack/solid-router 2.0.0-alpha.6 → 2.0.0-alpha.7

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 (219) hide show
  1. package/dist/cjs/Asset.cjs +2 -2
  2. package/dist/cjs/Asset.cjs.map +1 -1
  3. package/dist/cjs/HeadContent.cjs +11 -1
  4. package/dist/cjs/HeadContent.cjs.map +1 -1
  5. package/dist/cjs/HeadContent.dev.cjs +11 -1
  6. package/dist/cjs/HeadContent.dev.cjs.map +1 -1
  7. package/dist/cjs/Match.cjs +265 -248
  8. package/dist/cjs/Match.cjs.map +1 -1
  9. package/dist/cjs/Match.d.cts +1 -3
  10. package/dist/cjs/Matches.cjs +35 -34
  11. package/dist/cjs/Matches.cjs.map +1 -1
  12. package/dist/cjs/RouterProvider.cjs +12 -8
  13. package/dist/cjs/RouterProvider.cjs.map +1 -1
  14. package/dist/cjs/RouterProvider.d.cts +1 -1
  15. package/dist/cjs/Scripts.cjs +23 -12
  16. package/dist/cjs/Scripts.cjs.map +1 -1
  17. package/dist/cjs/Scripts.d.cts +2 -1
  18. package/dist/cjs/Transitioner.cjs +55 -34
  19. package/dist/cjs/Transitioner.cjs.map +1 -1
  20. package/dist/cjs/headContentUtils.cjs +26 -23
  21. package/dist/cjs/headContentUtils.cjs.map +1 -1
  22. package/dist/cjs/headContentUtils.d.cts +2 -1
  23. package/dist/cjs/index.cjs +1 -1
  24. package/dist/cjs/index.dev.cjs +1 -1
  25. package/dist/cjs/link.cjs +143 -101
  26. package/dist/cjs/link.cjs.map +1 -1
  27. package/dist/cjs/matchContext.cjs +7 -5
  28. package/dist/cjs/matchContext.cjs.map +1 -1
  29. package/dist/cjs/matchContext.d.cts +8 -2
  30. package/dist/cjs/not-found.cjs +8 -4
  31. package/dist/cjs/not-found.cjs.map +1 -1
  32. package/dist/cjs/not-found.d.cts +1 -1
  33. package/dist/cjs/router.cjs +2 -1
  34. package/dist/cjs/router.cjs.map +1 -1
  35. package/dist/cjs/routerStores.cjs +75 -0
  36. package/dist/cjs/routerStores.cjs.map +1 -0
  37. package/dist/cjs/routerStores.d.cts +10 -0
  38. package/dist/cjs/ssr/RouterClient.cjs +1 -1
  39. package/dist/cjs/ssr/RouterClient.cjs.map +1 -1
  40. package/dist/cjs/ssr/renderRouterToStream.cjs +1 -1
  41. package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -1
  42. package/dist/cjs/ssr/renderRouterToString.cjs +1 -1
  43. package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -1
  44. package/dist/cjs/useBlocker.cjs +12 -3
  45. package/dist/cjs/useBlocker.cjs.map +1 -1
  46. package/dist/cjs/useCanGoBack.cjs +6 -2
  47. package/dist/cjs/useCanGoBack.cjs.map +1 -1
  48. package/dist/cjs/useCanGoBack.d.cts +2 -1
  49. package/dist/cjs/useLoaderDeps.cjs +2 -3
  50. package/dist/cjs/useLoaderDeps.cjs.map +1 -1
  51. package/dist/cjs/useLocation.cjs +13 -2
  52. package/dist/cjs/useLocation.cjs.map +1 -1
  53. package/dist/cjs/useMatch.cjs +27 -15
  54. package/dist/cjs/useMatch.cjs.map +1 -1
  55. package/dist/cjs/useParams.cjs +1 -1
  56. package/dist/cjs/useParams.cjs.map +1 -1
  57. package/dist/cjs/useRouterState.cjs +12 -30
  58. package/dist/cjs/useRouterState.cjs.map +1 -1
  59. package/dist/cjs/useSearch.cjs +2 -1
  60. package/dist/cjs/useSearch.cjs.map +1 -1
  61. package/dist/cjs/utils.cjs +3 -17
  62. package/dist/cjs/utils.cjs.map +1 -1
  63. package/dist/cjs/utils.d.cts +0 -5
  64. package/dist/esm/Asset.js +6 -6
  65. package/dist/esm/Asset.js.map +1 -1
  66. package/dist/esm/HeadContent.dev.js +12 -2
  67. package/dist/esm/HeadContent.dev.js.map +1 -1
  68. package/dist/esm/HeadContent.js +12 -2
  69. package/dist/esm/HeadContent.js.map +1 -1
  70. package/dist/esm/Match.d.ts +1 -3
  71. package/dist/esm/Match.js +267 -250
  72. package/dist/esm/Match.js.map +1 -1
  73. package/dist/esm/Matches.js +40 -39
  74. package/dist/esm/Matches.js.map +1 -1
  75. package/dist/esm/RouterProvider.d.ts +1 -1
  76. package/dist/esm/RouterProvider.js +10 -7
  77. package/dist/esm/RouterProvider.js.map +1 -1
  78. package/dist/esm/ScriptOnce.js +2 -2
  79. package/dist/esm/ScriptOnce.js.map +1 -1
  80. package/dist/esm/Scripts.d.ts +2 -1
  81. package/dist/esm/Scripts.js +21 -11
  82. package/dist/esm/Scripts.js.map +1 -1
  83. package/dist/esm/Transitioner.js +56 -35
  84. package/dist/esm/Transitioner.js.map +1 -1
  85. package/dist/esm/headContentUtils.d.ts +2 -1
  86. package/dist/esm/headContentUtils.js +26 -23
  87. package/dist/esm/headContentUtils.js.map +1 -1
  88. package/dist/esm/index.dev.js +1 -1
  89. package/dist/esm/index.js +1 -1
  90. package/dist/esm/link.js +146 -104
  91. package/dist/esm/link.js.map +1 -1
  92. package/dist/esm/matchContext.d.ts +8 -2
  93. package/dist/esm/matchContext.js +7 -4
  94. package/dist/esm/matchContext.js.map +1 -1
  95. package/dist/esm/not-found.d.ts +1 -1
  96. package/dist/esm/not-found.js +6 -3
  97. package/dist/esm/not-found.js.map +1 -1
  98. package/dist/esm/router.js +2 -1
  99. package/dist/esm/router.js.map +1 -1
  100. package/dist/esm/routerStores.d.ts +10 -0
  101. package/dist/esm/routerStores.js +73 -0
  102. package/dist/esm/routerStores.js.map +1 -0
  103. package/dist/esm/scroll-restoration.js +2 -2
  104. package/dist/esm/scroll-restoration.js.map +1 -1
  105. package/dist/esm/ssr/RouterClient.js +1 -1
  106. package/dist/esm/ssr/RouterClient.js.map +1 -1
  107. package/dist/esm/ssr/renderRouterToStream.js +1 -1
  108. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  109. package/dist/esm/ssr/renderRouterToString.js +1 -1
  110. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  111. package/dist/esm/useBlocker.js +12 -3
  112. package/dist/esm/useBlocker.js.map +1 -1
  113. package/dist/esm/useCanGoBack.d.ts +2 -1
  114. package/dist/esm/useCanGoBack.js +4 -2
  115. package/dist/esm/useCanGoBack.js.map +1 -1
  116. package/dist/esm/useLoaderDeps.js +2 -3
  117. package/dist/esm/useLoaderDeps.js.map +1 -1
  118. package/dist/esm/useLocation.js +11 -2
  119. package/dist/esm/useLocation.js.map +1 -1
  120. package/dist/esm/useMatch.js +28 -16
  121. package/dist/esm/useMatch.js.map +1 -1
  122. package/dist/esm/useParams.js +1 -1
  123. package/dist/esm/useParams.js.map +1 -1
  124. package/dist/esm/useRouterState.js +11 -30
  125. package/dist/esm/useRouterState.js.map +1 -1
  126. package/dist/esm/useSearch.js +2 -1
  127. package/dist/esm/useSearch.js.map +1 -1
  128. package/dist/esm/utils.d.ts +0 -5
  129. package/dist/esm/utils.js +4 -17
  130. package/dist/esm/utils.js.map +1 -1
  131. package/dist/source/Asset.jsx +3 -3
  132. package/dist/source/Asset.jsx.map +1 -1
  133. package/dist/source/HeadContent.dev.jsx +5 -1
  134. package/dist/source/HeadContent.dev.jsx.map +1 -1
  135. package/dist/source/HeadContent.jsx +5 -1
  136. package/dist/source/HeadContent.jsx.map +1 -1
  137. package/dist/source/Match.d.ts +1 -3
  138. package/dist/source/Match.jsx +260 -264
  139. package/dist/source/Match.jsx.map +1 -1
  140. package/dist/source/Matches.jsx +46 -46
  141. package/dist/source/Matches.jsx.map +1 -1
  142. package/dist/source/RouterProvider.d.ts +1 -1
  143. package/dist/source/RouterProvider.jsx +13 -9
  144. package/dist/source/RouterProvider.jsx.map +1 -1
  145. package/dist/source/Scripts.d.ts +2 -1
  146. package/dist/source/Scripts.jsx +46 -47
  147. package/dist/source/Scripts.jsx.map +1 -1
  148. package/dist/source/Transitioner.jsx +78 -42
  149. package/dist/source/Transitioner.jsx.map +1 -1
  150. package/dist/source/headContentUtils.d.ts +2 -1
  151. package/dist/source/headContentUtils.jsx +79 -80
  152. package/dist/source/headContentUtils.jsx.map +1 -1
  153. package/dist/source/link.jsx +145 -112
  154. package/dist/source/link.jsx.map +1 -1
  155. package/dist/source/matchContext.d.ts +8 -2
  156. package/dist/source/matchContext.jsx +7 -3
  157. package/dist/source/matchContext.jsx.map +1 -1
  158. package/dist/source/not-found.d.ts +1 -1
  159. package/dist/source/not-found.jsx +6 -5
  160. package/dist/source/not-found.jsx.map +1 -1
  161. package/dist/source/router.js +2 -1
  162. package/dist/source/router.js.map +1 -1
  163. package/dist/source/routerStores.d.ts +10 -0
  164. package/dist/source/routerStores.js +82 -0
  165. package/dist/source/routerStores.js.map +1 -0
  166. package/dist/source/ssr/RouterClient.jsx +1 -1
  167. package/dist/source/ssr/RouterClient.jsx.map +1 -1
  168. package/dist/source/ssr/renderRouterToStream.jsx +1 -1
  169. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -1
  170. package/dist/source/ssr/renderRouterToString.jsx +1 -1
  171. package/dist/source/ssr/renderRouterToString.jsx.map +1 -1
  172. package/dist/source/useBlocker.jsx +19 -8
  173. package/dist/source/useBlocker.jsx.map +1 -1
  174. package/dist/source/useCanGoBack.d.ts +2 -1
  175. package/dist/source/useCanGoBack.js +4 -2
  176. package/dist/source/useCanGoBack.js.map +1 -1
  177. package/dist/source/useLoaderDeps.jsx +2 -3
  178. package/dist/source/useLoaderDeps.jsx.map +1 -1
  179. package/dist/source/useLocation.jsx +13 -3
  180. package/dist/source/useLocation.jsx.map +1 -1
  181. package/dist/source/useMatch.jsx +33 -23
  182. package/dist/source/useMatch.jsx.map +1 -1
  183. package/dist/source/useParams.jsx +1 -1
  184. package/dist/source/useParams.jsx.map +1 -1
  185. package/dist/source/useRouterState.jsx +14 -55
  186. package/dist/source/useRouterState.jsx.map +1 -1
  187. package/dist/source/useSearch.jsx +2 -1
  188. package/dist/source/useSearch.jsx.map +1 -1
  189. package/dist/source/utils.d.ts +0 -5
  190. package/dist/source/utils.js +2 -15
  191. package/dist/source/utils.js.map +1 -1
  192. package/package.json +2 -2
  193. package/skills/solid-router/SKILL.md +2 -0
  194. package/src/Asset.tsx +3 -3
  195. package/src/HeadContent.dev.tsx +10 -1
  196. package/src/HeadContent.tsx +10 -1
  197. package/src/Match.tsx +395 -349
  198. package/src/Matches.tsx +55 -54
  199. package/src/RouterProvider.tsx +13 -10
  200. package/src/Scripts.tsx +55 -54
  201. package/src/Transitioner.tsx +101 -58
  202. package/src/headContentUtils.tsx +104 -96
  203. package/src/link.tsx +188 -146
  204. package/src/matchContext.tsx +16 -7
  205. package/src/not-found.tsx +6 -6
  206. package/src/router.ts +2 -1
  207. package/src/routerStores.ts +119 -0
  208. package/src/ssr/RouterClient.tsx +1 -1
  209. package/src/ssr/renderRouterToStream.tsx +1 -1
  210. package/src/ssr/renderRouterToString.tsx +1 -1
  211. package/src/useBlocker.tsx +80 -63
  212. package/src/useCanGoBack.ts +6 -2
  213. package/src/useLoaderDeps.tsx +2 -3
  214. package/src/useLocation.tsx +18 -5
  215. package/src/useMatch.tsx +37 -38
  216. package/src/useParams.tsx +2 -3
  217. package/src/useRouterState.tsx +21 -67
  218. package/src/useSearch.tsx +2 -1
  219. package/src/utils.ts +2 -24
package/src/link.tsx CHANGED
@@ -11,7 +11,6 @@ import {
11
11
 
12
12
  import { isServer } from '@tanstack/router-core/isServer'
13
13
  import { Dynamic } from '@solidjs/web'
14
- import { useRouterState } from './useRouterState'
15
14
  import { useRouter } from './useRouter'
16
15
 
17
16
  import { useIntersectionObserver } from './utils'
@@ -75,8 +74,8 @@ export function useLinkProps<
75
74
  const [local, rest] = splitProps(
76
75
  Solid.merge(
77
76
  {
78
- activeProps: () => ({ class: 'active' }),
79
- inactiveProps: () => ({}),
77
+ activeProps: STATIC_ACTIVE_PROPS_GET,
78
+ inactiveProps: STATIC_INACTIVE_PROPS_GET,
80
79
  },
81
80
  options,
82
81
  ),
@@ -118,34 +117,20 @@ export function useLinkProps<
118
117
  'unsafeRelative',
119
118
  ] as any)
120
119
 
121
- const currentLocation = useRouterState({
122
- select: (s) => s.location,
123
- })
124
-
125
- const buildLocationKey = useRouterState({
126
- select: (s) => {
127
- const leaf = s.matches[s.matches.length - 1]
128
- return {
129
- search: leaf?.search,
130
- hash: s.location.hash,
131
- path: leaf?.pathname, // path + params
132
- }
133
- },
134
- })
120
+ const currentLocation = Solid.createMemo(
121
+ () => router.stores.location.state,
122
+ undefined,
123
+ { equals: (prev, next) => prev?.href === next?.href },
124
+ )
135
125
 
136
- const from = options.from
137
-
138
- const _options = () => {
139
- const result = {
140
- ...options,
141
- from,
142
- }
143
- return result
144
- }
126
+ const _options = () => options
145
127
 
146
128
  const next = Solid.createMemo(() => {
147
- buildLocationKey()
148
- return router.buildLocation(_options() as any)
129
+ // Rebuild when inherited search/hash or the current route context changes.
130
+ const _fromLocation = currentLocation()
131
+ const options = { _fromLocation, ..._options() } as any
132
+ // untrack because router-core will also access stores, which are signals in solid
133
+ return Solid.untrack(() => router.buildLocation(options))
149
134
  })
150
135
 
151
136
  const hrefOption = Solid.createMemo(() => {
@@ -181,15 +166,13 @@ export function useLinkProps<
181
166
  return _href.href
182
167
  }
183
168
  const to = _options().to
184
- const isSafeInternal =
185
- typeof to === 'string' &&
186
- to.charCodeAt(0) === 47 && // '/'
187
- to.charCodeAt(1) !== 47 // but not '//'
188
- if (isSafeInternal) return undefined
169
+ const safeInternal = isSafeInternal(to)
170
+ if (safeInternal) return undefined
171
+ if (typeof to !== 'string' || to.indexOf(':') === -1) return undefined
189
172
  try {
190
173
  new URL(to as any)
191
174
  // Block dangerous protocols like javascript:, blob:, data:
192
- if (isDangerousProtocol(to as string, router.protocolAllowlist)) {
175
+ if (isDangerousProtocol(to, router.protocolAllowlist)) {
193
176
  if (process.env.NODE_ENV !== 'production') {
194
177
  console.warn(`Blocked Link with dangerous protocol: ${to}`)
195
178
  }
@@ -211,56 +194,60 @@ export function useLinkProps<
211
194
 
212
195
  const isActive = Solid.createMemo(() => {
213
196
  if (externalLink()) return false
214
- if (local.activeOptions?.exact) {
197
+ const activeOptions = local.activeOptions
198
+ const current = currentLocation()
199
+ const nextLocation = next()
200
+
201
+ if (activeOptions?.exact) {
215
202
  const testExact = exactPathTest(
216
- currentLocation().pathname,
217
- next().pathname,
203
+ current.pathname,
204
+ nextLocation.pathname,
218
205
  router.basepath,
219
206
  )
220
207
  if (!testExact) {
221
208
  return false
222
209
  }
223
210
  } else {
224
- const currentPathSplit = removeTrailingSlash(
225
- currentLocation().pathname,
211
+ const currentPath = removeTrailingSlash(current.pathname, router.basepath)
212
+ const nextPath = removeTrailingSlash(
213
+ nextLocation.pathname,
226
214
  router.basepath,
227
- ).split('/')
228
- const nextPathSplit = removeTrailingSlash(
229
- next()?.pathname,
230
- router.basepath,
231
- )?.split('/')
232
-
233
- const pathIsFuzzyEqual = nextPathSplit?.every(
234
- (d, i) => d === currentPathSplit[i],
235
215
  )
216
+
217
+ const pathIsFuzzyEqual =
218
+ currentPath.startsWith(nextPath) &&
219
+ (currentPath.length === nextPath.length ||
220
+ currentPath[nextPath.length] === '/')
236
221
  if (!pathIsFuzzyEqual) {
237
222
  return false
238
223
  }
239
224
  }
240
225
 
241
- if (local.activeOptions?.includeSearch ?? true) {
242
- const searchTest = deepEqual(currentLocation().search, next().search, {
243
- partial: !local.activeOptions?.exact,
244
- ignoreUndefined: !local.activeOptions?.explicitUndefined,
226
+ if (activeOptions?.includeSearch ?? true) {
227
+ const searchTest = deepEqual(current.search, nextLocation.search, {
228
+ partial: !activeOptions?.exact,
229
+ ignoreUndefined: !activeOptions?.explicitUndefined,
245
230
  })
246
231
  if (!searchTest) {
247
232
  return false
248
233
  }
249
234
  }
250
235
 
251
- if (local.activeOptions?.includeHash) {
236
+ if (activeOptions?.includeHash) {
252
237
  const currentHash =
253
- shouldHydrateHash && !hasHydrated() ? '' : currentLocation().hash
254
- return currentHash === next().hash
238
+ shouldHydrateHash && !hasHydrated() ? '' : current.hash
239
+ return currentHash === nextLocation.hash
255
240
  }
256
241
  return true
257
242
  })
258
243
 
259
244
  const doPreload = () =>
260
- router.preloadRoute(_options() as any).catch((err: any) => {
261
- console.warn(err)
262
- console.warn(preloadWarning)
263
- })
245
+ router
246
+ .preloadRoute({ ..._options(), _builtLocation: next() } as any)
247
+ .catch((err: any) => {
248
+ console.warn(err)
249
+ console.warn(preloadWarning)
250
+ })
264
251
 
265
252
  const preloadViewportIoCallback = (
266
253
  entry: IntersectionObserverEntry | undefined,
@@ -290,17 +277,18 @@ export function useLinkProps<
290
277
  return
291
278
  }
292
279
  if (!local.disabled && preloadValue === 'render') {
293
- doPreload()
280
+ Solid.untrack(() => doPreload())
294
281
  hasRenderFetched = true
295
282
  }
296
283
  })
297
284
 
298
285
  if (Solid.untrack(externalLink)) {
286
+ const externalHref = Solid.untrack(externalLink)
299
287
  return Solid.merge(
300
288
  propsSafeToSpread,
301
289
  {
302
290
  // ref: mergeRefs(setRef, _options().ref),
303
- href: externalLink(),
291
+ href: externalHref,
304
292
  },
305
293
  splitProps(local, [
306
294
  'target',
@@ -337,11 +325,15 @@ export function useLinkProps<
337
325
  ) {
338
326
  e.preventDefault()
339
327
 
340
- setIsTransitioning(true)
328
+ Solid.runWithOwner(null, () => {
329
+ setIsTransitioning(true)
330
+ })
341
331
 
342
332
  const unsub = router.subscribe('onResolved', () => {
343
333
  unsub()
344
- setIsTransitioning(false)
334
+ Solid.runWithOwner(null, () => {
335
+ setIsTransitioning(false)
336
+ })
345
337
  })
346
338
 
347
339
  // All is well? Navigate!
@@ -395,100 +387,139 @@ export function useLinkProps<
395
387
  }
396
388
  }
397
389
 
398
- /** Call a JSX.EventHandlerUnion with the event. */
399
- function callHandler<T, TEvent extends Event>(
400
- event: TEvent & { currentTarget: T; target: Element },
401
- handler: Solid.JSX.EventHandlerUnion<T, TEvent> | undefined,
402
- ) {
403
- if (handler) {
404
- if (typeof handler === 'function') {
405
- handler(event)
406
- } else {
407
- handler[0](handler[1], event)
408
- }
409
- }
390
+ const simpleStyling = Solid.createMemo(
391
+ () =>
392
+ local.activeProps === STATIC_ACTIVE_PROPS_GET &&
393
+ local.inactiveProps === STATIC_INACTIVE_PROPS_GET &&
394
+ local.class === undefined &&
395
+ local.style === undefined,
396
+ )
397
+
398
+ const onClick = createComposedHandler(() => local.onClick, handleClick)
399
+ const onBlur = createComposedHandler(() => local.onBlur, handleLeave)
400
+ const onFocus = createComposedHandler(
401
+ () => local.onFocus,
402
+ enqueueIntentPreload,
403
+ )
404
+ const onMouseEnter = createComposedHandler(
405
+ () => local.onMouseEnter,
406
+ enqueueIntentPreload,
407
+ )
408
+ const onMouseOver = createComposedHandler(
409
+ () => local.onMouseOver,
410
+ enqueueIntentPreload,
411
+ )
412
+ const onMouseLeave = createComposedHandler(
413
+ () => local.onMouseLeave,
414
+ handleLeave,
415
+ )
416
+ const onMouseOut = createComposedHandler(() => local.onMouseOut, handleLeave)
417
+ const onTouchStart = createComposedHandler(
418
+ () => local.onTouchStart,
419
+ handleTouchStart,
420
+ )
410
421
 
411
- return event.defaultPrevented
422
+ type ResolvedLinkStateProps = Omit<Solid.ComponentProps<'a'>, 'style'> & {
423
+ style?: Solid.JSX.CSSProperties
412
424
  }
413
425
 
414
- function composeEventHandlers<T>(
415
- handlers: Array<Solid.JSX.EventHandlerUnion<T, any> | undefined>,
416
- ) {
417
- return (event: any) => {
418
- for (const handler of handlers) {
419
- callHandler(event, handler)
426
+ const resolvedProps = Solid.createMemo(() => {
427
+ const active = isActive()
428
+
429
+ const base = {
430
+ href: hrefOption()?.href,
431
+ ref: mergeRefs(setRef, _options().ref as any),
432
+ onClick,
433
+ onBlur,
434
+ onFocus,
435
+ onMouseEnter,
436
+ onMouseOver,
437
+ onMouseLeave,
438
+ onMouseOut,
439
+ onTouchStart,
440
+ disabled: !!local.disabled,
441
+ target: local.target,
442
+ ...(local.disabled && STATIC_DISABLED_PROPS),
443
+ ...(isTransitioning() && STATIC_TRANSITIONING_ATTRIBUTES),
444
+ }
445
+
446
+ if (simpleStyling()) {
447
+ return {
448
+ ...base,
449
+ ...(active && STATIC_DEFAULT_ACTIVE_ATTRIBUTES),
420
450
  }
421
451
  }
422
- }
423
452
 
424
- // Get the active props
425
- const resolvedActiveProps: () => Omit<Solid.ComponentProps<'a'>, 'style'> & {
426
- style?: Solid.JSX.CSSProperties
427
- } = () =>
428
- isActive() ? (functionalUpdate(local.activeProps as any, {}) ?? {}) : {}
429
-
430
- // Get the inactive props
431
- const resolvedInactiveProps: () => Omit<
432
- Solid.ComponentProps<'a'>,
433
- 'style'
434
- > & { style?: Solid.JSX.CSSProperties } = () =>
435
- isActive() ? {} : functionalUpdate(local.inactiveProps, {})
436
-
437
- const resolvedClassName = () =>
438
- [local.class, resolvedActiveProps().class, resolvedInactiveProps().class]
453
+ const activeProps: ResolvedLinkStateProps = active
454
+ ? (functionalUpdate(local.activeProps as any, {}) ?? EMPTY_OBJECT)
455
+ : EMPTY_OBJECT
456
+ const inactiveProps: ResolvedLinkStateProps = active
457
+ ? EMPTY_OBJECT
458
+ : functionalUpdate(local.inactiveProps, {})
459
+ const style = {
460
+ ...local.style,
461
+ ...activeProps.style,
462
+ ...inactiveProps.style,
463
+ }
464
+ const className = [local.class, activeProps.class, inactiveProps.class]
439
465
  .filter(Boolean)
440
466
  .join(' ')
441
467
 
442
- const resolvedStyle = () => ({
443
- ...local.style,
444
- ...resolvedActiveProps().style,
445
- ...resolvedInactiveProps().style,
468
+ return {
469
+ ...activeProps,
470
+ ...inactiveProps,
471
+ ...base,
472
+ ...(Object.keys(style).length ? { style } : undefined),
473
+ ...(className ? { class: className } : undefined),
474
+ ...(active && STATIC_ACTIVE_ATTRIBUTES),
475
+ } as ResolvedLinkStateProps
446
476
  })
447
477
 
448
- return Solid.merge(
449
- propsSafeToSpread,
450
- resolvedActiveProps,
451
- resolvedInactiveProps,
452
- () => {
453
- return {
454
- href: hrefOption()?.href,
455
- ref: mergeRefs(setRef, (_options() as any).ref),
456
- onClick: composeEventHandlers([local.onClick, handleClick]),
457
- onBlur: composeEventHandlers([local.onBlur, handleLeave]),
458
- onFocus: composeEventHandlers([local.onFocus, enqueueIntentPreload]),
459
- onMouseEnter: composeEventHandlers([
460
- local.onMouseEnter,
461
- enqueueIntentPreload,
462
- ]),
463
- onMouseOver: composeEventHandlers([
464
- local.onMouseOver,
465
- enqueueIntentPreload,
466
- ]),
467
- onMouseLeave: composeEventHandlers([local.onMouseLeave, handleLeave]),
468
- onMouseOut: composeEventHandlers([local.onMouseOut, handleLeave]),
469
- onTouchStart: composeEventHandlers([
470
- local.onTouchStart,
471
- handleTouchStart,
472
- ]),
473
- disabled: !!local.disabled,
474
- target: local.target,
475
- ...(() => {
476
- const s = resolvedStyle()
477
- return Object.keys(s).length ? { style: s } : {}
478
- })(),
479
- ...(() => {
480
- const c = resolvedClassName()
481
- return c ? { class: c } : {}
482
- })(),
483
- ...(local.disabled && {
484
- role: 'link',
485
- 'aria-disabled': 'true',
486
- }),
487
- ...(isActive() && { 'data-status': 'active', 'aria-current': 'page' }),
488
- ...(isTransitioning() && { 'data-transitioning': 'transitioning' }),
489
- }
490
- },
491
- ) as any
478
+ return Solid.merge(propsSafeToSpread, resolvedProps) as any
479
+ }
480
+
481
+ const STATIC_ACTIVE_PROPS = { class: 'active' }
482
+ const STATIC_ACTIVE_PROPS_GET = () => STATIC_ACTIVE_PROPS
483
+ const EMPTY_OBJECT = {}
484
+ const STATIC_INACTIVE_PROPS_GET = () => EMPTY_OBJECT
485
+ const STATIC_DEFAULT_ACTIVE_ATTRIBUTES = {
486
+ class: 'active',
487
+ 'data-status': 'active',
488
+ 'aria-current': 'page',
489
+ }
490
+ const STATIC_DISABLED_PROPS = {
491
+ role: 'link',
492
+ 'aria-disabled': 'true',
493
+ }
494
+ const STATIC_ACTIVE_ATTRIBUTES = {
495
+ 'data-status': 'active',
496
+ 'aria-current': 'page',
497
+ }
498
+ const STATIC_TRANSITIONING_ATTRIBUTES = {
499
+ 'data-transitioning': 'transitioning',
500
+ }
501
+
502
+ /** Call a JSX.EventHandlerUnion with the event. */
503
+ function callHandler<T, TEvent extends Event>(
504
+ event: TEvent & { currentTarget: T; target: Element },
505
+ handler: Solid.JSX.EventHandlerUnion<T, TEvent>,
506
+ ) {
507
+ if (typeof handler === 'function') {
508
+ handler(event)
509
+ } else {
510
+ handler[0](handler[1], event)
511
+ }
512
+ return event.defaultPrevented
513
+ }
514
+
515
+ function createComposedHandler<T, TEvent extends Event>(
516
+ getHandler: () => Solid.JSX.EventHandlerUnion<T, TEvent> | undefined,
517
+ fallback: (event: TEvent) => void,
518
+ ) {
519
+ return (event: TEvent & { currentTarget: T; target: Element }) => {
520
+ const handler = getHandler()
521
+ if (!handler || !callHandler(event, handler)) fallback(event)
522
+ }
492
523
  }
493
524
 
494
525
  export type UseLinkPropsOptions<
@@ -653,8 +684,12 @@ export const Link: LinkComponent<'a'> = (props) => {
653
684
  )
654
685
  }
655
686
 
687
+ if (!local._asChild) {
688
+ return <a {...linkProps}>{children()}</a>
689
+ }
690
+
656
691
  return (
657
- <Dynamic component={local._asChild ? local._asChild : 'a'} {...linkProps}>
692
+ <Dynamic component={local._asChild as Solid.ValidComponent} {...linkProps}>
658
693
  {children()}
659
694
  </Dynamic>
660
695
  )
@@ -664,6 +699,13 @@ function isCtrlEvent(e: MouseEvent) {
664
699
  return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
665
700
  }
666
701
 
702
+ function isSafeInternal(to: unknown) {
703
+ if (typeof to !== 'string') return false
704
+ const zero = to.charCodeAt(0)
705
+ if (zero === 47) return to.charCodeAt(1) !== 47 // '/' but not '//'
706
+ return zero === 46 // '.', '..', './', '../'
707
+ }
708
+
667
709
  export type LinkOptionsFnOptions<
668
710
  TOptions,
669
711
  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,119 @@
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 as any)
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
+ let depth = 0
102
+
103
+ return {
104
+ createMutableStore: createSolidMutableStore,
105
+ createReadonlyStore: createSolidReadonlyStore,
106
+
107
+ batch: (fn) => {
108
+ depth++
109
+ fn()
110
+ depth--
111
+ if (depth === 0) {
112
+ try {
113
+ Solid.flush()
114
+ } catch {}
115
+ }
116
+ },
117
+ init: (stores) => initRouterStores(stores, createSolidReadonlyStore),
118
+ }
119
+ }
@@ -7,7 +7,7 @@ let hydrationPromise: Promise<void | Array<Array<void>>> | undefined
7
7
 
8
8
  export function RouterClient(props: { router: AnyRouter }) {
9
9
  if (!hydrationPromise) {
10
- if (!props.router.state.matches.length) {
10
+ if (!props.router.stores.matchesId.state.length) {
11
11
  hydrationPromise = hydrate(props.router)
12
12
  } else {
13
13
  hydrationPromise = Promise.resolve()
@@ -42,7 +42,7 @@ export const renderRouterToStream = async ({
42
42
  readable as unknown as ReadableStream,
43
43
  )
44
44
  return new Response(responseStream as any, {
45
- status: router.state.statusCode,
45
+ status: router.stores.statusCode.state,
46
46
  headers: responseHeaders,
47
47
  })
48
48
  }
@@ -32,7 +32,7 @@ export const renderRouterToString = ({
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) {