@tanstack/router-core 1.132.0-alpha.8 → 1.132.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 (92) hide show
  1. package/dist/cjs/Matches.cjs +2 -1
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/fileRoute.d.cts +3 -3
  4. package/dist/cjs/index.cjs +7 -2
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/index.d.cts +8 -4
  7. package/dist/cjs/load-matches.cjs +5 -3
  8. package/dist/cjs/load-matches.cjs.map +1 -1
  9. package/dist/cjs/location.d.cts +38 -0
  10. package/dist/cjs/path.cjs +27 -64
  11. package/dist/cjs/path.cjs.map +1 -1
  12. package/dist/cjs/path.d.cts +6 -7
  13. package/dist/cjs/process-route-tree.cjs +144 -0
  14. package/dist/cjs/process-route-tree.cjs.map +1 -0
  15. package/dist/cjs/process-route-tree.d.cts +10 -0
  16. package/dist/cjs/redirect.cjs +1 -1
  17. package/dist/cjs/redirect.cjs.map +1 -1
  18. package/dist/cjs/rewrite.cjs +63 -0
  19. package/dist/cjs/rewrite.cjs.map +1 -0
  20. package/dist/cjs/rewrite.d.cts +22 -0
  21. package/dist/cjs/route.cjs.map +1 -1
  22. package/dist/cjs/route.d.cts +55 -42
  23. package/dist/cjs/router.cjs +77 -184
  24. package/dist/cjs/router.cjs.map +1 -1
  25. package/dist/cjs/router.d.cts +68 -37
  26. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  27. package/dist/cjs/scroll-restoration.d.cts +9 -0
  28. package/dist/cjs/ssr/createRequestHandler.cjs +4 -1
  29. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
  30. package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
  31. package/dist/cjs/ssr/serializer/transformer.d.cts +10 -8
  32. package/dist/cjs/ssr/server.d.cts +0 -5
  33. package/dist/cjs/ssr/ssr-server.cjs +5 -2
  34. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  35. package/dist/cjs/ssr/ssr-server.d.cts +4 -1
  36. package/dist/cjs/utils.cjs +68 -46
  37. package/dist/cjs/utils.cjs.map +1 -1
  38. package/dist/esm/Matches.js +2 -1
  39. package/dist/esm/Matches.js.map +1 -1
  40. package/dist/esm/fileRoute.d.ts +3 -3
  41. package/dist/esm/index.d.ts +8 -4
  42. package/dist/esm/index.js +8 -3
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/load-matches.js +5 -3
  45. package/dist/esm/load-matches.js.map +1 -1
  46. package/dist/esm/location.d.ts +38 -0
  47. package/dist/esm/path.d.ts +6 -7
  48. package/dist/esm/path.js +27 -64
  49. package/dist/esm/path.js.map +1 -1
  50. package/dist/esm/process-route-tree.d.ts +10 -0
  51. package/dist/esm/process-route-tree.js +144 -0
  52. package/dist/esm/process-route-tree.js.map +1 -0
  53. package/dist/esm/redirect.js +1 -1
  54. package/dist/esm/redirect.js.map +1 -1
  55. package/dist/esm/rewrite.d.ts +22 -0
  56. package/dist/esm/rewrite.js +63 -0
  57. package/dist/esm/rewrite.js.map +1 -0
  58. package/dist/esm/route.d.ts +55 -42
  59. package/dist/esm/route.js.map +1 -1
  60. package/dist/esm/router.d.ts +68 -37
  61. package/dist/esm/router.js +79 -186
  62. package/dist/esm/router.js.map +1 -1
  63. package/dist/esm/scroll-restoration.d.ts +9 -0
  64. package/dist/esm/scroll-restoration.js.map +1 -1
  65. package/dist/esm/ssr/createRequestHandler.js +4 -1
  66. package/dist/esm/ssr/createRequestHandler.js.map +1 -1
  67. package/dist/esm/ssr/serializer/transformer.d.ts +10 -8
  68. package/dist/esm/ssr/serializer/transformer.js.map +1 -1
  69. package/dist/esm/ssr/server.d.ts +0 -5
  70. package/dist/esm/ssr/ssr-server.d.ts +4 -1
  71. package/dist/esm/ssr/ssr-server.js +5 -2
  72. package/dist/esm/ssr/ssr-server.js.map +1 -1
  73. package/dist/esm/utils.js +68 -46
  74. package/dist/esm/utils.js.map +1 -1
  75. package/package.json +2 -2
  76. package/src/Matches.ts +2 -1
  77. package/src/fileRoute.ts +16 -6
  78. package/src/index.ts +11 -6
  79. package/src/load-matches.ts +29 -19
  80. package/src/location.ts +38 -0
  81. package/src/path.ts +44 -82
  82. package/src/process-route-tree.ts +233 -0
  83. package/src/redirect.ts +1 -1
  84. package/src/rewrite.ts +70 -0
  85. package/src/route.ts +214 -80
  86. package/src/router.ts +208 -329
  87. package/src/scroll-restoration.ts +1 -1
  88. package/src/ssr/createRequestHandler.ts +4 -1
  89. package/src/ssr/serializer/transformer.ts +17 -17
  90. package/src/ssr/server.ts +5 -5
  91. package/src/ssr/ssr-server.ts +8 -5
  92. package/src/utils.ts +83 -61
package/src/index.ts CHANGED
@@ -103,7 +103,6 @@ export {
103
103
  parsePathname,
104
104
  interpolatePath,
105
105
  matchPathname,
106
- removeBasepath,
107
106
  matchByPath,
108
107
  } from './path'
109
108
  export type { Segment } from './path'
@@ -175,7 +174,6 @@ export type {
175
174
  RouteLoaderFn,
176
175
  LoaderFnContext,
177
176
  RouteContextFn,
178
- BeforeLoadFn,
179
177
  ContextOptions,
180
178
  RouteContextOptions,
181
179
  BeforeLoadContextOptions,
@@ -193,8 +191,10 @@ export type {
193
191
  ResolveOptionalParams,
194
192
  ResolveRequiredParams,
195
193
  RootRoute,
194
+ FilebaseRouteOptionsInterface,
196
195
  } from './route'
197
-
196
+ export { processRouteTree } from './process-route-tree'
197
+ export type { ProcessRouteTreeResult } from './process-route-tree'
198
198
  export {
199
199
  defaultSerializeError,
200
200
  getLocationChangeInfo,
@@ -203,7 +203,6 @@ export {
203
203
  SearchParamError,
204
204
  PathParamError,
205
205
  getInitialRouterState,
206
- processRouteTree,
207
206
  getMatchedRoutes,
208
207
  } from './router'
209
208
  export type {
@@ -244,9 +243,7 @@ export type {
244
243
  LoadRouteChunkFn,
245
244
  ClearCacheFn,
246
245
  CreateRouterFn,
247
- ProcessRouteTreeResult,
248
246
  SSROption,
249
- DefaultRegister,
250
247
  } from './router'
251
248
 
252
249
  export * from './config'
@@ -432,3 +429,11 @@ export {
432
429
  } from './ssr/serializer/transformer'
433
430
 
434
431
  export { defaultSerovalPlugins } from './ssr/serializer/seroval-plugins'
432
+
433
+ export {
434
+ rewriteBasepath,
435
+ composeRewrites,
436
+ executeRewriteInput,
437
+ executeRewriteOutput,
438
+ } from './rewrite'
439
+ export type { LocationRewrite, LocationRewriteFunction } from './router'
@@ -406,23 +406,32 @@ const executeBeforeLoad = (
406
406
 
407
407
  const { search, params, cause } = match
408
408
  const preload = resolvePreload(inner, matchId)
409
- const beforeLoadFnContext: BeforeLoadContextOptions<any, any, any, any, any> =
410
- {
411
- search,
412
- abortController,
413
- params,
414
- preload,
415
- context,
416
- location: inner.location,
417
- navigate: (opts: any) =>
418
- inner.router.navigate({
419
- ...opts,
420
- _fromLocation: inner.location,
421
- }),
422
- buildLocation: inner.router.buildLocation,
423
- cause: preload ? 'preload' : cause,
424
- matches: inner.matches,
425
- }
409
+ const beforeLoadFnContext: BeforeLoadContextOptions<
410
+ any,
411
+ any,
412
+ any,
413
+ any,
414
+ any,
415
+ any,
416
+ any,
417
+ any
418
+ > = {
419
+ search,
420
+ abortController,
421
+ params,
422
+ preload,
423
+ context,
424
+ location: inner.location,
425
+ navigate: (opts: any) =>
426
+ inner.router.navigate({
427
+ ...opts,
428
+ _fromLocation: inner.location,
429
+ }),
430
+ buildLocation: inner.router.buildLocation,
431
+ cause: preload ? 'preload' : cause,
432
+ matches: inner.matches,
433
+ ...inner.router.options.additionalContext,
434
+ }
426
435
 
427
436
  const updateContext = (beforeLoadContext: any) => {
428
437
  if (beforeLoadContext === undefined) {
@@ -487,14 +496,14 @@ const handleBeforeLoad = (
487
496
  return queueExecution()
488
497
  }
489
498
 
499
+ const execute = () => executeBeforeLoad(inner, matchId, index, route)
500
+
490
501
  const queueExecution = () => {
491
502
  if (shouldSkipLoader(inner, matchId)) return
492
503
  const result = preBeforeLoadSetup(inner, matchId, route)
493
504
  return isPromise(result) ? result.then(execute) : execute()
494
505
  }
495
506
 
496
- const execute = () => executeBeforeLoad(inner, matchId, index, route)
497
-
498
507
  return serverSsr()
499
508
  }
500
509
 
@@ -571,6 +580,7 @@ const getLoaderContext = (
571
580
  }),
572
581
  cause: preload ? 'preload' : cause,
573
582
  route,
583
+ ...inner.router.options.additionalContext,
574
584
  }
575
585
  }
576
586
 
package/src/location.ts CHANGED
@@ -2,12 +2,50 @@ import type { ParsedHistoryState } from '@tanstack/history'
2
2
  import type { AnySchema } from './validators'
3
3
 
4
4
  export interface ParsedLocation<TSearchObj extends AnySchema = {}> {
5
+ /**
6
+ * The full path of the location, including pathname, search, and hash.
7
+ * Does not include the origin. Is the equivalent of calling
8
+ * `url.replace(url.origin, '')`
9
+ */
5
10
  href: string
11
+ /**
12
+ * @description The pathname of the location, including the leading slash.
13
+ */
6
14
  pathname: string
15
+ /**
16
+ * The parsed search parameters of the location in object form.
17
+ */
7
18
  search: TSearchObj
19
+ /**
20
+ * The search string of the location, including the leading question mark.
21
+ */
8
22
  searchStr: string
23
+ /**
24
+ * The in-memory state of the location as it *may* exist in the browser's history.
25
+ */
9
26
  state: ParsedHistoryState
27
+ /**
28
+ * The hash of the location, including the leading hash character.
29
+ */
10
30
  hash: string
31
+ /**
32
+ * The masked location of the location.
33
+ */
11
34
  maskedLocation?: ParsedLocation<TSearchObj>
35
+ /**
36
+ * Whether to unmask the location on reload.
37
+ */
12
38
  unmaskOnReload?: boolean
39
+ /**
40
+ * @private
41
+ * @description The public href of the location, including the origin before any rewrites.
42
+ * If a rewrite is applied, the `href` property will be the rewritten URL.
43
+ */
44
+ publicHref: string
45
+ /**
46
+ * @private
47
+ * @description The full URL of the location, including the origin.
48
+ * @private
49
+ */
50
+ url: string
13
51
  }
package/src/path.ts CHANGED
@@ -97,11 +97,9 @@ export function exactPathTest(
97
97
  // /a/b/c + d/ = /a/b/c/d
98
98
  // /a/b/c + d/e = /a/b/c/d/e
99
99
  interface ResolvePathOptions {
100
- basepath: string
101
100
  base: string
102
101
  to: string
103
102
  trailingSlash?: 'always' | 'never' | 'preserve'
104
- caseSensitive?: boolean
105
103
  parseCache?: ParsePathnameCache
106
104
  }
107
105
 
@@ -151,18 +149,13 @@ function segmentToString(segment: Segment): string {
151
149
  }
152
150
 
153
151
  export function resolvePath({
154
- basepath,
155
152
  base,
156
153
  to,
157
154
  trailingSlash = 'never',
158
- caseSensitive,
159
155
  parseCache,
160
156
  }: ResolvePathOptions) {
161
- base = removeBasepath(basepath, base, caseSensitive)
162
- to = removeBasepath(basepath, to, caseSensitive)
163
-
164
- let baseSegments = parsePathname(base, parseCache).slice()
165
- const toSegments = parsePathname(to, parseCache)
157
+ let baseSegments = parseBasePathSegments(base, parseCache).slice()
158
+ const toSegments = parseRoutePathSegments(to, parseCache)
166
159
 
167
160
  if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
168
161
  baseSegments.pop()
@@ -201,19 +194,32 @@ export function resolvePath({
201
194
  }
202
195
 
203
196
  const segmentValues = baseSegments.map(segmentToString)
204
- const joined = joinPaths([basepath, ...segmentValues])
197
+ // const joined = joinPaths([basepath, ...segmentValues])
198
+ const joined = joinPaths(segmentValues)
205
199
  return joined
206
200
  }
207
201
 
208
202
  export type ParsePathnameCache = LRUCache<string, ReadonlyArray<Segment>>
203
+
204
+ export const parseBasePathSegments = (
205
+ pathname?: string,
206
+ cache?: ParsePathnameCache,
207
+ ): ReadonlyArray<Segment> => parsePathname(pathname, cache, true)
208
+
209
+ export const parseRoutePathSegments = (
210
+ pathname?: string,
211
+ cache?: ParsePathnameCache,
212
+ ): ReadonlyArray<Segment> => parsePathname(pathname, cache, false)
213
+
209
214
  export const parsePathname = (
210
215
  pathname?: string,
211
216
  cache?: ParsePathnameCache,
217
+ basePathValues?: boolean,
212
218
  ): ReadonlyArray<Segment> => {
213
219
  if (!pathname) return []
214
220
  const cached = cache?.get(pathname)
215
221
  if (cached) return cached
216
- const parsed = baseParsePathname(pathname)
222
+ const parsed = baseParsePathname(pathname, basePathValues)
217
223
  cache?.set(pathname, parsed)
218
224
  return parsed
219
225
  }
@@ -243,7 +249,10 @@ const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
243
249
  * - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
244
250
  * - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
245
251
  */
246
- function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
252
+ function baseParsePathname(
253
+ pathname: string,
254
+ basePathValues?: boolean,
255
+ ): ReadonlyArray<Segment> {
247
256
  pathname = cleanPath(pathname)
248
257
 
249
258
  const segments: Array<Segment> = []
@@ -265,8 +274,12 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
265
274
 
266
275
  segments.push(
267
276
  ...split.map((part): Segment => {
277
+ // strip tailing underscore for non-nested paths
278
+ const partToMatch =
279
+ !basePathValues && part.slice(-1) === '_' ? part.slice(0, -1) : part
280
+
268
281
  // Check for wildcard with curly braces: prefix{$}suffix
269
- const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE)
282
+ const wildcardBracesMatch = partToMatch.match(WILDCARD_W_CURLY_BRACES_RE)
270
283
  if (wildcardBracesMatch) {
271
284
  const prefix = wildcardBracesMatch[1]
272
285
  const suffix = wildcardBracesMatch[2]
@@ -279,7 +292,7 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
279
292
  }
280
293
 
281
294
  // Check for optional parameter format: prefix{-$paramName}suffix
282
- const optionalParamBracesMatch = part.match(
295
+ const optionalParamBracesMatch = partToMatch.match(
283
296
  OPTIONAL_PARAM_W_CURLY_BRACES_RE,
284
297
  )
285
298
  if (optionalParamBracesMatch) {
@@ -295,7 +308,7 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
295
308
  }
296
309
 
297
310
  // Check for the new parameter format: prefix{$paramName}suffix
298
- const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
311
+ const paramBracesMatch = partToMatch.match(PARAM_W_CURLY_BRACES_RE)
299
312
  if (paramBracesMatch) {
300
313
  const prefix = paramBracesMatch[1]
301
314
  const paramName = paramBracesMatch[2]
@@ -309,8 +322,8 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
309
322
  }
310
323
 
311
324
  // Check for bare parameter format: $paramName (without curly braces)
312
- if (PARAM_RE.test(part)) {
313
- const paramName = part.substring(1)
325
+ if (PARAM_RE.test(partToMatch)) {
326
+ const paramName = partToMatch.substring(1)
314
327
  return {
315
328
  type: SEGMENT_TYPE_PARAM,
316
329
  value: '$' + paramName,
@@ -320,7 +333,7 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
320
333
  }
321
334
 
322
335
  // Check for bare wildcard: $ (without curly braces)
323
- if (WILDCARD_RE.test(part)) {
336
+ if (WILDCARD_RE.test(partToMatch)) {
324
337
  return {
325
338
  type: SEGMENT_TYPE_WILDCARD,
326
339
  value: '$',
@@ -332,12 +345,12 @@ function baseParsePathname(pathname: string): ReadonlyArray<Segment> {
332
345
  // Handle regular pathname segment
333
346
  return {
334
347
  type: SEGMENT_TYPE_PATHNAME,
335
- value: part.includes('%25')
336
- ? part
348
+ value: partToMatch.includes('%25')
349
+ ? partToMatch
337
350
  .split('%25')
338
351
  .map((segment) => decodeURI(segment))
339
352
  .join('%25')
340
- : decodeURI(part),
353
+ : decodeURI(partToMatch),
341
354
  }
342
355
  }),
343
356
  )
@@ -376,7 +389,7 @@ export function interpolatePath({
376
389
  decodeCharMap,
377
390
  parseCache,
378
391
  }: InterpolatePathOptions): InterPolatePathResult {
379
- const interpolatedPathSegments = parsePathname(path, parseCache)
392
+ const interpolatedPathSegments = parseRoutePathSegments(path, parseCache)
380
393
 
381
394
  function encodeParam(key: string): any {
382
395
  const value = params[key]
@@ -403,6 +416,10 @@ export function interpolatePath({
403
416
 
404
417
  if (segment.type === SEGMENT_TYPE_WILDCARD) {
405
418
  usedParams._splat = params._splat
419
+
420
+ // TODO: Deprecate *
421
+ usedParams['*'] = params._splat
422
+
406
423
  const segmentPrefix = segment.prefixSegment || ''
407
424
  const segmentSuffix = segment.suffixSegment || ''
408
425
 
@@ -491,17 +508,11 @@ function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
491
508
  }
492
509
 
493
510
  export function matchPathname(
494
- basepath: string,
495
511
  currentPathname: string,
496
512
  matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
497
513
  parseCache?: ParsePathnameCache,
498
514
  ): AnyPathParams | undefined {
499
- const pathParams = matchByPath(
500
- basepath,
501
- currentPathname,
502
- matchLocation,
503
- parseCache,
504
- )
515
+ const pathParams = matchByPath(currentPathname, matchLocation, parseCache)
505
516
  // const searchMatched = matchBySearch(location.search, matchLocation)
506
517
 
507
518
  if (matchLocation.to && !pathParams) {
@@ -511,49 +522,7 @@ export function matchPathname(
511
522
  return pathParams ?? {}
512
523
  }
513
524
 
514
- export function removeBasepath(
515
- basepath: string,
516
- pathname: string,
517
- caseSensitive: boolean = false,
518
- ) {
519
- // normalize basepath and pathname for case-insensitive comparison if needed
520
- const normalizedBasepath = caseSensitive ? basepath : basepath.toLowerCase()
521
- const normalizedPathname = caseSensitive ? pathname : pathname.toLowerCase()
522
-
523
- switch (true) {
524
- // default behaviour is to serve app from the root - pathname
525
- // left untouched
526
- case normalizedBasepath === '/':
527
- return pathname
528
-
529
- // shortcut for removing the basepath if it matches the pathname
530
- case normalizedPathname === normalizedBasepath:
531
- return ''
532
-
533
- // in case pathname is shorter than basepath - there is
534
- // nothing to remove
535
- case pathname.length < basepath.length:
536
- return pathname
537
-
538
- // avoid matching partial segments - strict equality handled
539
- // earlier, otherwise, basepath separated from pathname with
540
- // separator, therefore lack of separator means partial
541
- // segment match (`/app` should not match `/application`)
542
- case normalizedPathname[normalizedBasepath.length] !== '/':
543
- return pathname
544
-
545
- // remove the basepath from the pathname if it starts with it
546
- case normalizedPathname.startsWith(normalizedBasepath):
547
- return pathname.slice(basepath.length)
548
-
549
- // otherwise, return the pathname as is
550
- default:
551
- return pathname
552
- }
553
- }
554
-
555
525
  export function matchByPath(
556
- basepath: string,
557
526
  from: string,
558
527
  {
559
528
  to,
@@ -562,22 +531,15 @@ export function matchByPath(
562
531
  }: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
563
532
  parseCache?: ParsePathnameCache,
564
533
  ): Record<string, string> | undefined {
565
- // check basepath first
566
- if (basepath !== '/' && !from.startsWith(basepath)) {
567
- return undefined
568
- }
569
- // Remove the base path from the pathname
570
- from = removeBasepath(basepath, from, caseSensitive)
571
- // Default to to $ (wildcard)
572
- to = removeBasepath(basepath, `${to ?? '$'}`, caseSensitive)
534
+ const stringTo = to as string
573
535
 
574
536
  // Parse the from and to
575
- const baseSegments = parsePathname(
537
+ const baseSegments = parseBasePathSegments(
576
538
  from.startsWith('/') ? from : `/${from}`,
577
539
  parseCache,
578
540
  )
579
- const routeSegments = parsePathname(
580
- to.startsWith('/') ? to : `/${to}`,
541
+ const routeSegments = parseRoutePathSegments(
542
+ stringTo.startsWith('/') ? stringTo : `/${stringTo}`,
581
543
  parseCache,
582
544
  )
583
545
 
@@ -0,0 +1,233 @@
1
+ import invariant from 'tiny-invariant'
2
+ import {
3
+ SEGMENT_TYPE_OPTIONAL_PARAM,
4
+ SEGMENT_TYPE_PARAM,
5
+ SEGMENT_TYPE_PATHNAME,
6
+ parseRoutePathSegments,
7
+ trimPathLeft,
8
+ trimPathRight,
9
+ } from './path'
10
+ import type { Segment } from './path'
11
+ import type { RouteLike } from './route'
12
+
13
+ const SLASH_SCORE = 0.75
14
+ const STATIC_SEGMENT_SCORE = 1
15
+ const REQUIRED_PARAM_BASE_SCORE = 0.5
16
+ const OPTIONAL_PARAM_BASE_SCORE = 0.4
17
+ const WILDCARD_PARAM_BASE_SCORE = 0.25
18
+ const STATIC_AFTER_DYNAMIC_BONUS_SCORE = 0.2
19
+ const BOTH_PRESENCE_BASE_SCORE = 0.05
20
+ const PREFIX_PRESENCE_BASE_SCORE = 0.02
21
+ const SUFFIX_PRESENCE_BASE_SCORE = 0.01
22
+ const PREFIX_LENGTH_SCORE_MULTIPLIER = 0.0002
23
+ const SUFFIX_LENGTH_SCORE_MULTIPLIER = 0.0001
24
+
25
+ function handleParam(segment: Segment, baseScore: number) {
26
+ if (segment.prefixSegment && segment.suffixSegment) {
27
+ return (
28
+ baseScore +
29
+ BOTH_PRESENCE_BASE_SCORE +
30
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length +
31
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
32
+ )
33
+ }
34
+
35
+ if (segment.prefixSegment) {
36
+ return (
37
+ baseScore +
38
+ PREFIX_PRESENCE_BASE_SCORE +
39
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length
40
+ )
41
+ }
42
+
43
+ if (segment.suffixSegment) {
44
+ return (
45
+ baseScore +
46
+ SUFFIX_PRESENCE_BASE_SCORE +
47
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
48
+ )
49
+ }
50
+
51
+ return baseScore
52
+ }
53
+
54
+ function sortRoutes<TRouteLike extends RouteLike>(
55
+ routes: ReadonlyArray<TRouteLike>,
56
+ ): Array<TRouteLike> {
57
+ const scoredRoutes: Array<{
58
+ child: TRouteLike
59
+ trimmed: string
60
+ parsed: ReadonlyArray<Segment>
61
+ index: number
62
+ scores: Array<number>
63
+ hasStaticAfter: boolean
64
+ optionalParamCount: number
65
+ }> = []
66
+
67
+ routes.forEach((d, i) => {
68
+ if (d.isRoot || !d.path) {
69
+ return
70
+ }
71
+
72
+ const trimmed = trimPathLeft(d.fullPath)
73
+ let parsed = parseRoutePathSegments(trimmed)
74
+
75
+ // Removes the leading slash if it is not the only remaining segment
76
+ let skip = 0
77
+ while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
78
+ skip++
79
+ }
80
+ if (skip > 0) parsed = parsed.slice(skip)
81
+
82
+ let optionalParamCount = 0
83
+ let hasStaticAfter = false
84
+ const scores = parsed.map((segment, index) => {
85
+ if (segment.value === '/') {
86
+ return SLASH_SCORE
87
+ }
88
+
89
+ if (segment.type === SEGMENT_TYPE_PATHNAME) {
90
+ return STATIC_SEGMENT_SCORE
91
+ }
92
+
93
+ let baseScore: number | undefined = undefined
94
+ if (segment.type === SEGMENT_TYPE_PARAM) {
95
+ baseScore = REQUIRED_PARAM_BASE_SCORE
96
+ } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
97
+ baseScore = OPTIONAL_PARAM_BASE_SCORE
98
+ optionalParamCount++
99
+ } else {
100
+ baseScore = WILDCARD_PARAM_BASE_SCORE
101
+ }
102
+
103
+ // if there is any static segment (that is not an index) after a required / optional param,
104
+ // we will boost this param so it ranks higher than a required/optional param without a static segment after it
105
+ // JUST FOR SORTING, NOT FOR MATCHING
106
+ for (let i = index + 1; i < parsed.length; i++) {
107
+ const nextSegment = parsed[i]!
108
+ if (
109
+ nextSegment.type === SEGMENT_TYPE_PATHNAME &&
110
+ nextSegment.value !== '/'
111
+ ) {
112
+ hasStaticAfter = true
113
+ return handleParam(
114
+ segment,
115
+ baseScore + STATIC_AFTER_DYNAMIC_BONUS_SCORE,
116
+ )
117
+ }
118
+ }
119
+
120
+ return handleParam(segment, baseScore)
121
+ })
122
+
123
+ scoredRoutes.push({
124
+ child: d,
125
+ trimmed,
126
+ parsed,
127
+ index: i,
128
+ scores,
129
+ optionalParamCount,
130
+ hasStaticAfter,
131
+ })
132
+ })
133
+
134
+ const flatRoutes = scoredRoutes
135
+ .sort((a, b) => {
136
+ const minLength = Math.min(a.scores.length, b.scores.length)
137
+
138
+ // Sort by segment-by-segment score comparison ONLY for the common prefix
139
+ for (let i = 0; i < minLength; i++) {
140
+ if (a.scores[i] !== b.scores[i]) {
141
+ return b.scores[i]! - a.scores[i]!
142
+ }
143
+ }
144
+
145
+ // If all common segments have equal scores, then consider length and specificity
146
+ if (a.scores.length !== b.scores.length) {
147
+ // If different number of optional parameters, fewer optional parameters wins (more specific)
148
+ // only if both or none of the routes has static segments after the params
149
+ if (a.optionalParamCount !== b.optionalParamCount) {
150
+ if (a.hasStaticAfter === b.hasStaticAfter) {
151
+ return a.optionalParamCount - b.optionalParamCount
152
+ } else if (a.hasStaticAfter && !b.hasStaticAfter) {
153
+ return -1
154
+ } else if (!a.hasStaticAfter && b.hasStaticAfter) {
155
+ return 1
156
+ }
157
+ }
158
+
159
+ // If same number of optional parameters, longer path wins (for static segments)
160
+ return b.scores.length - a.scores.length
161
+ }
162
+
163
+ // Sort by min available parsed value for alphabetical ordering
164
+ for (let i = 0; i < minLength; i++) {
165
+ if (a.parsed[i]!.value !== b.parsed[i]!.value) {
166
+ return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
167
+ }
168
+ }
169
+
170
+ // Sort by original index
171
+ return a.index - b.index
172
+ })
173
+ .map((d, i) => {
174
+ d.child.rank = i
175
+ return d.child
176
+ })
177
+
178
+ return flatRoutes
179
+ }
180
+
181
+ export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
182
+ routesById: Record<string, TRouteLike>
183
+ routesByPath: Record<string, TRouteLike>
184
+ flatRoutes: Array<TRouteLike>
185
+ }
186
+
187
+ export function processRouteTree<TRouteLike extends RouteLike>({
188
+ routeTree,
189
+ initRoute,
190
+ }: {
191
+ routeTree: TRouteLike
192
+ initRoute?: (route: TRouteLike, index: number) => void
193
+ }): ProcessRouteTreeResult<TRouteLike> {
194
+ const routesById = {} as Record<string, TRouteLike>
195
+ const routesByPath = {} as Record<string, TRouteLike>
196
+
197
+ const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
198
+ childRoutes.forEach((childRoute, i) => {
199
+ initRoute?.(childRoute, i)
200
+
201
+ const existingRoute = routesById[childRoute.id]
202
+
203
+ invariant(
204
+ !existingRoute,
205
+ `Duplicate routes found with id: ${String(childRoute.id)}`,
206
+ )
207
+
208
+ routesById[childRoute.id] = childRoute
209
+
210
+ if (!childRoute.isRoot && childRoute.path) {
211
+ const trimmedFullPath = trimPathRight(childRoute.fullPath)
212
+ if (
213
+ !routesByPath[trimmedFullPath] ||
214
+ childRoute.fullPath.endsWith('/')
215
+ ) {
216
+ routesByPath[trimmedFullPath] = childRoute
217
+ }
218
+ }
219
+
220
+ const children = childRoute.children as Array<TRouteLike>
221
+
222
+ if (children?.length) {
223
+ recurseRoutes(children)
224
+ }
225
+ })
226
+ }
227
+
228
+ recurseRoutes([routeTree])
229
+
230
+ const flatRoutes = sortRoutes(Object.values(routesById))
231
+
232
+ return { routesById, routesByPath, flatRoutes }
233
+ }
package/src/redirect.ts CHANGED
@@ -72,7 +72,7 @@ export function redirect<
72
72
  } catch {}
73
73
  }
74
74
 
75
- const headers = new Headers(opts.headers || {})
75
+ const headers = new Headers(opts.headers)
76
76
  if (opts.href && headers.get('Location') === null) {
77
77
  headers.set('Location', opts.href)
78
78
  }