@tanstack/router-core 1.132.0-alpha.4 → 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.
- package/dist/cjs/Matches.cjs +2 -1
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -2
- package/dist/cjs/config.cjs +10 -0
- package/dist/cjs/config.cjs.map +1 -0
- package/dist/cjs/config.d.cts +17 -0
- package/dist/cjs/fileRoute.d.cts +3 -2
- package/dist/cjs/index.cjs +9 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +10 -5
- package/dist/cjs/load-matches.cjs +5 -3
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/location.d.cts +38 -0
- package/dist/cjs/path.cjs +27 -64
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +6 -7
- package/dist/cjs/process-route-tree.cjs +144 -0
- package/dist/cjs/process-route-tree.cjs.map +1 -0
- package/dist/cjs/process-route-tree.d.cts +10 -0
- package/dist/cjs/redirect.cjs +1 -1
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/rewrite.cjs +63 -0
- package/dist/cjs/rewrite.cjs.map +1 -0
- package/dist/cjs/rewrite.d.cts +22 -0
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +62 -44
- package/dist/cjs/router.cjs +102 -210
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +81 -44
- package/dist/cjs/scroll-restoration.cjs.map +1 -1
- package/dist/cjs/scroll-restoration.d.cts +9 -0
- package/dist/cjs/ssr/createRequestHandler.cjs +4 -1
- package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/transformer.cjs +14 -12
- package/dist/cjs/ssr/serializer/transformer.cjs.map +1 -1
- package/dist/cjs/ssr/serializer/transformer.d.cts +55 -15
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +5 -2
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.d.cts +4 -1
- package/dist/cjs/utils.cjs +68 -46
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/Matches.d.ts +2 -2
- package/dist/esm/Matches.js +2 -1
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/config.d.ts +17 -0
- package/dist/esm/config.js +10 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/fileRoute.d.ts +3 -2
- package/dist/esm/index.d.ts +10 -5
- package/dist/esm/index.js +10 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/load-matches.js +5 -3
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/location.d.ts +38 -0
- package/dist/esm/path.d.ts +6 -7
- package/dist/esm/path.js +27 -64
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/process-route-tree.d.ts +10 -0
- package/dist/esm/process-route-tree.js +144 -0
- package/dist/esm/process-route-tree.js.map +1 -0
- package/dist/esm/redirect.js +1 -1
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/rewrite.d.ts +22 -0
- package/dist/esm/rewrite.js +63 -0
- package/dist/esm/rewrite.js.map +1 -0
- package/dist/esm/route.d.ts +62 -44
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +81 -44
- package/dist/esm/router.js +104 -212
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/scroll-restoration.d.ts +9 -0
- package/dist/esm/scroll-restoration.js.map +1 -1
- package/dist/esm/ssr/createRequestHandler.js +4 -1
- package/dist/esm/ssr/createRequestHandler.js.map +1 -1
- package/dist/esm/ssr/serializer/transformer.d.ts +55 -15
- package/dist/esm/ssr/serializer/transformer.js +14 -12
- package/dist/esm/ssr/serializer/transformer.js.map +1 -1
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.d.ts +4 -1
- package/dist/esm/ssr/ssr-server.js +5 -2
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/utils.js +68 -46
- package/dist/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/Matches.ts +4 -3
- package/src/config.ts +42 -0
- package/src/fileRoute.ts +25 -3
- package/src/index.ts +23 -6
- package/src/load-matches.ts +31 -21
- package/src/location.ts +38 -0
- package/src/path.ts +44 -82
- package/src/process-route-tree.ts +233 -0
- package/src/redirect.ts +1 -1
- package/src/rewrite.ts +70 -0
- package/src/route.ts +311 -74
- package/src/router.ts +263 -389
- package/src/scroll-restoration.ts +1 -1
- package/src/ssr/createRequestHandler.ts +4 -1
- package/src/ssr/serializer/transformer.ts +168 -31
- package/src/ssr/server.ts +6 -0
- package/src/ssr/ssr-client.ts +2 -2
- package/src/ssr/ssr-server.ts +10 -7
- package/src/utils.ts +83 -61
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
|
-
|
|
162
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
313
|
-
const paramName =
|
|
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(
|
|
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:
|
|
336
|
-
?
|
|
348
|
+
value: partToMatch.includes('%25')
|
|
349
|
+
? partToMatch
|
|
337
350
|
.split('%25')
|
|
338
351
|
.map((segment) => decodeURI(segment))
|
|
339
352
|
.join('%25')
|
|
340
|
-
: decodeURI(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
537
|
+
const baseSegments = parseBasePathSegments(
|
|
576
538
|
from.startsWith('/') ? from : `/${from}`,
|
|
577
539
|
parseCache,
|
|
578
540
|
)
|
|
579
|
-
const routeSegments =
|
|
580
|
-
|
|
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
package/src/rewrite.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { joinPaths, trimPath } from './path'
|
|
2
|
+
import type { LocationRewrite } from './router'
|
|
3
|
+
|
|
4
|
+
export function composeRewrites(rewrites: Array<LocationRewrite>) {
|
|
5
|
+
return {
|
|
6
|
+
input: ({ url }) => {
|
|
7
|
+
for (const rewrite of rewrites) {
|
|
8
|
+
url = executeRewriteInput(rewrite, url)
|
|
9
|
+
}
|
|
10
|
+
return url
|
|
11
|
+
},
|
|
12
|
+
output: ({ url }) => {
|
|
13
|
+
for (let i = rewrites.length - 1; i >= 0; i--) {
|
|
14
|
+
url = executeRewriteOutput(rewrites[i], url)
|
|
15
|
+
}
|
|
16
|
+
return url
|
|
17
|
+
},
|
|
18
|
+
} satisfies LocationRewrite
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rewriteBasepath(opts: {
|
|
22
|
+
basepath: string
|
|
23
|
+
caseSensitive?: boolean
|
|
24
|
+
}) {
|
|
25
|
+
const trimmedBasepath = trimPath(opts.basepath)
|
|
26
|
+
const regex = new RegExp(
|
|
27
|
+
`^/${trimmedBasepath}/`,
|
|
28
|
+
opts.caseSensitive ? '' : 'i',
|
|
29
|
+
)
|
|
30
|
+
return {
|
|
31
|
+
input: ({ url }) => {
|
|
32
|
+
url.pathname = url.pathname.replace(regex, '/')
|
|
33
|
+
return url
|
|
34
|
+
},
|
|
35
|
+
output: ({ url }) => {
|
|
36
|
+
url.pathname = joinPaths(['/', trimmedBasepath, url.pathname])
|
|
37
|
+
return url
|
|
38
|
+
},
|
|
39
|
+
} satisfies LocationRewrite
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function executeRewriteInput(
|
|
43
|
+
rewrite: LocationRewrite | undefined,
|
|
44
|
+
url: URL,
|
|
45
|
+
): URL {
|
|
46
|
+
const res = rewrite?.input?.({ url })
|
|
47
|
+
if (res) {
|
|
48
|
+
if (typeof res === 'string') {
|
|
49
|
+
return new URL(res)
|
|
50
|
+
} else if (res instanceof URL) {
|
|
51
|
+
return res
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return url
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function executeRewriteOutput(
|
|
58
|
+
rewrite: LocationRewrite | undefined,
|
|
59
|
+
url: URL,
|
|
60
|
+
): URL {
|
|
61
|
+
const res = rewrite?.output?.({ url })
|
|
62
|
+
if (res) {
|
|
63
|
+
if (typeof res === 'string') {
|
|
64
|
+
return new URL(res)
|
|
65
|
+
} else if (res instanceof URL) {
|
|
66
|
+
return res
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return url
|
|
70
|
+
}
|