@tanstack/router-core 0.0.1-beta.8 → 1.20.3-alpha.1
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/cjs/Matches.cjs +13 -0
- package/dist/cjs/Matches.cjs.map +1 -0
- package/dist/cjs/Matches.d.cts +109 -0
- package/dist/cjs/RouterProvider.d.cts +26 -0
- package/dist/cjs/defer.cjs +25 -0
- package/dist/cjs/defer.cjs.map +1 -0
- package/dist/cjs/defer.d.cts +20 -0
- package/dist/cjs/fileRoute.d.cts +23 -0
- package/dist/cjs/history.d.cts +8 -0
- package/dist/cjs/index.cjs +80 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +41 -0
- package/dist/cjs/link.cjs +5 -0
- package/dist/cjs/link.cjs.map +1 -0
- package/dist/cjs/link.d.cts +200 -0
- package/dist/cjs/location.d.cts +12 -0
- package/dist/cjs/manifest.d.cts +24 -0
- package/dist/cjs/not-found.cjs +13 -0
- package/dist/cjs/not-found.cjs.map +1 -0
- package/dist/cjs/not-found.d.cts +20 -0
- package/dist/cjs/path.cjs +412 -0
- package/dist/cjs/path.cjs.map +1 -0
- package/dist/cjs/path.d.cts +56 -0
- package/dist/cjs/qss.cjs +38 -0
- package/dist/cjs/qss.cjs.map +1 -0
- package/dist/cjs/qss.d.cts +22 -0
- package/dist/cjs/redirect.cjs +34 -0
- package/dist/cjs/redirect.cjs.map +1 -0
- package/dist/cjs/redirect.d.cts +38 -0
- package/dist/cjs/root.cjs +5 -0
- package/dist/cjs/root.cjs.map +1 -0
- package/dist/cjs/root.d.cts +2 -0
- package/dist/cjs/route.cjs +119 -0
- package/dist/cjs/route.cjs.map +1 -0
- package/dist/cjs/route.d.cts +422 -0
- package/dist/cjs/routeInfo.d.cts +54 -0
- package/dist/cjs/router.cjs +1800 -0
- package/dist/cjs/router.cjs.map +1 -0
- package/dist/cjs/router.d.cts +630 -0
- package/dist/cjs/scroll-restoration.cjs +196 -0
- package/dist/cjs/scroll-restoration.cjs.map +1 -0
- package/dist/cjs/scroll-restoration.d.cts +38 -0
- package/dist/cjs/searchMiddleware.cjs +42 -0
- package/dist/cjs/searchMiddleware.cjs.map +1 -0
- package/dist/cjs/searchMiddleware.d.cts +5 -0
- package/dist/cjs/searchParams.cjs +61 -0
- package/dist/cjs/searchParams.cjs.map +1 -0
- package/dist/cjs/searchParams.d.cts +7 -0
- package/dist/cjs/serializer.d.cts +22 -0
- package/dist/cjs/structuralSharing.d.cts +4 -0
- package/dist/cjs/typePrimitives.d.cts +65 -0
- package/dist/cjs/useLoaderData.d.cts +5 -0
- package/dist/cjs/useLoaderDeps.d.cts +5 -0
- package/dist/cjs/useNavigate.d.cts +3 -0
- package/dist/cjs/useParams.d.cts +5 -0
- package/dist/cjs/useRouteContext.d.cts +9 -0
- package/dist/cjs/useSearch.d.cts +5 -0
- package/dist/cjs/utils.cjs +160 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +105 -0
- package/dist/cjs/validators.d.cts +51 -0
- package/dist/esm/Matches.d.ts +109 -0
- package/dist/esm/Matches.js +13 -0
- package/dist/esm/Matches.js.map +1 -0
- package/dist/esm/RouterProvider.d.ts +26 -0
- package/dist/esm/defer.d.ts +20 -0
- package/dist/esm/defer.js +25 -0
- package/dist/esm/defer.js.map +1 -0
- package/dist/esm/fileRoute.d.ts +23 -0
- package/dist/esm/history.d.ts +8 -0
- package/dist/esm/index.d.ts +41 -0
- package/dist/esm/index.js +80 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/link.d.ts +200 -0
- package/dist/esm/link.js +5 -0
- package/dist/esm/link.js.map +1 -0
- package/dist/esm/location.d.ts +12 -0
- package/dist/esm/manifest.d.ts +24 -0
- package/dist/esm/not-found.d.ts +20 -0
- package/dist/esm/not-found.js +13 -0
- package/dist/esm/not-found.js.map +1 -0
- package/dist/esm/path.d.ts +56 -0
- package/dist/esm/path.js +412 -0
- package/dist/esm/path.js.map +1 -0
- package/dist/esm/qss.d.ts +22 -0
- package/dist/esm/qss.js +38 -0
- package/dist/esm/qss.js.map +1 -0
- package/dist/esm/redirect.d.ts +38 -0
- package/dist/esm/redirect.js +34 -0
- package/dist/esm/redirect.js.map +1 -0
- package/dist/esm/root.d.ts +2 -0
- package/dist/esm/root.js +5 -0
- package/dist/esm/root.js.map +1 -0
- package/dist/esm/route.d.ts +422 -0
- package/dist/esm/route.js +119 -0
- package/dist/esm/route.js.map +1 -0
- package/dist/esm/routeInfo.d.ts +54 -0
- package/dist/esm/router.d.ts +630 -0
- package/dist/esm/router.js +1800 -0
- package/dist/esm/router.js.map +1 -0
- package/dist/esm/scroll-restoration.d.ts +38 -0
- package/dist/esm/scroll-restoration.js +196 -0
- package/dist/esm/scroll-restoration.js.map +1 -0
- package/dist/esm/searchMiddleware.d.ts +5 -0
- package/dist/esm/searchMiddleware.js +42 -0
- package/dist/esm/searchMiddleware.js.map +1 -0
- package/dist/esm/searchParams.d.ts +7 -0
- package/dist/esm/searchParams.js +61 -0
- package/dist/esm/searchParams.js.map +1 -0
- package/dist/esm/serializer.d.ts +22 -0
- package/dist/esm/structuralSharing.d.ts +4 -0
- package/dist/esm/typePrimitives.d.ts +65 -0
- package/dist/esm/useLoaderData.d.ts +5 -0
- package/dist/esm/useLoaderDeps.d.ts +5 -0
- package/dist/esm/useNavigate.d.ts +3 -0
- package/dist/esm/useParams.d.ts +5 -0
- package/dist/esm/useRouteContext.d.ts +9 -0
- package/dist/esm/useSearch.d.ts +5 -0
- package/dist/esm/utils.d.ts +105 -0
- package/dist/esm/utils.js +160 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/validators.d.ts +51 -0
- package/package.json +36 -32
- package/src/Matches.ts +239 -0
- package/src/RouterProvider.ts +50 -0
- package/src/defer.ts +52 -0
- package/src/fileRoute.ts +140 -0
- package/src/history.ts +9 -0
- package/src/index.ts +421 -19
- package/src/link.ts +580 -286
- package/src/location.ts +13 -0
- package/src/manifest.ts +32 -0
- package/src/not-found.ts +29 -0
- package/src/path.ts +425 -49
- package/src/qss.ts +70 -41
- package/src/redirect.ts +100 -0
- package/src/root.ts +2 -0
- package/src/route.ts +1679 -228
- package/src/routeInfo.ts +224 -217
- package/src/router.ts +3073 -1033
- package/src/scroll-restoration.ts +340 -0
- package/src/searchMiddleware.ts +54 -0
- package/src/searchParams.ts +43 -20
- package/src/serializer.ts +32 -0
- package/src/structuralSharing.ts +7 -0
- package/src/typePrimitives.ts +181 -0
- package/src/useLoaderData.ts +20 -0
- package/src/useLoaderDeps.ts +13 -0
- package/src/useNavigate.ts +13 -0
- package/src/useParams.ts +20 -0
- package/src/useRouteContext.ts +39 -0
- package/src/useSearch.ts +20 -0
- package/src/utils.ts +369 -75
- package/src/validators.ts +121 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -33
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
- package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js +0 -33
- package/build/cjs/node_modules/@babel/runtime/helpers/esm/extends.js.map +0 -1
- package/build/cjs/node_modules/history/index.js +0 -815
- package/build/cjs/node_modules/history/index.js.map +0 -1
- package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js +0 -30
- package/build/cjs/node_modules/tiny-invariant/dist/esm/tiny-invariant.js.map +0 -1
- package/build/cjs/packages/router-core/src/index.js +0 -59
- package/build/cjs/packages/router-core/src/index.js.map +0 -1
- package/build/cjs/packages/router-core/src/path.js +0 -222
- package/build/cjs/packages/router-core/src/path.js.map +0 -1
- package/build/cjs/packages/router-core/src/qss.js +0 -71
- package/build/cjs/packages/router-core/src/qss.js.map +0 -1
- package/build/cjs/packages/router-core/src/route.js +0 -161
- package/build/cjs/packages/router-core/src/route.js.map +0 -1
- package/build/cjs/packages/router-core/src/routeConfig.js +0 -69
- package/build/cjs/packages/router-core/src/routeConfig.js.map +0 -1
- package/build/cjs/packages/router-core/src/routeMatch.js +0 -266
- package/build/cjs/packages/router-core/src/routeMatch.js.map +0 -1
- package/build/cjs/packages/router-core/src/router.js +0 -812
- package/build/cjs/packages/router-core/src/router.js.map +0 -1
- package/build/cjs/packages/router-core/src/searchParams.js +0 -70
- package/build/cjs/packages/router-core/src/searchParams.js.map +0 -1
- package/build/cjs/packages/router-core/src/utils.js +0 -125
- package/build/cjs/packages/router-core/src/utils.js.map +0 -1
- package/build/esm/index.js +0 -2480
- package/build/esm/index.js.map +0 -1
- package/build/stats-html.html +0 -4034
- package/build/stats-react.json +0 -499
- package/build/types/index.d.ts +0 -619
- package/build/umd/index.development.js +0 -2514
- package/build/umd/index.development.js.map +0 -1
- package/build/umd/index.production.js +0 -12
- package/build/umd/index.production.js.map +0 -1
- package/src/frameworks.ts +0 -12
- package/src/routeConfig.ts +0 -495
- package/src/routeMatch.ts +0 -374
package/src/location.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ParsedHistoryState } from '@tanstack/history'
|
|
2
|
+
import type { AnySchema } from './validators'
|
|
3
|
+
|
|
4
|
+
export interface ParsedLocation<TSearchObj extends AnySchema = {}> {
|
|
5
|
+
href: string
|
|
6
|
+
pathname: string
|
|
7
|
+
search: TSearchObj
|
|
8
|
+
searchStr: string
|
|
9
|
+
state: ParsedHistoryState
|
|
10
|
+
hash: string
|
|
11
|
+
maskedLocation?: ParsedLocation<TSearchObj>
|
|
12
|
+
unmaskOnReload?: boolean
|
|
13
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Manifest = {
|
|
2
|
+
routes: Record<
|
|
3
|
+
string,
|
|
4
|
+
{
|
|
5
|
+
filePath?: string
|
|
6
|
+
preloads?: Array<string>
|
|
7
|
+
assets?: Array<RouterManagedTag>
|
|
8
|
+
}
|
|
9
|
+
>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RouterManagedTag =
|
|
13
|
+
| {
|
|
14
|
+
tag: 'title'
|
|
15
|
+
attrs?: Record<string, any>
|
|
16
|
+
children: string
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
tag: 'meta' | 'link'
|
|
20
|
+
attrs?: Record<string, any>
|
|
21
|
+
children?: never
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
tag: 'script'
|
|
25
|
+
attrs?: Record<string, any>
|
|
26
|
+
children?: string
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
tag: 'style'
|
|
30
|
+
attrs?: Record<string, any>
|
|
31
|
+
children?: string
|
|
32
|
+
}
|
package/src/not-found.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RouteIds } from './routeInfo'
|
|
2
|
+
import type { RegisteredRouter } from './router'
|
|
3
|
+
|
|
4
|
+
export type NotFoundError = {
|
|
5
|
+
/**
|
|
6
|
+
@deprecated
|
|
7
|
+
Use `routeId: rootRouteId` instead
|
|
8
|
+
*/
|
|
9
|
+
global?: boolean
|
|
10
|
+
/**
|
|
11
|
+
@private
|
|
12
|
+
Do not use this. It's used internally to indicate a path matching error
|
|
13
|
+
*/
|
|
14
|
+
_global?: boolean
|
|
15
|
+
data?: any
|
|
16
|
+
throw?: boolean
|
|
17
|
+
routeId?: RouteIds<RegisteredRouter['routeTree']>
|
|
18
|
+
headers?: HeadersInit
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function notFound(options: NotFoundError = {}) {
|
|
22
|
+
;(options as any).isNotFound = true
|
|
23
|
+
if (options.throw) throw options
|
|
24
|
+
return options
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isNotFound(obj: any): obj is NotFoundError {
|
|
28
|
+
return !!obj?.isNotFound
|
|
29
|
+
}
|
package/src/path.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { AnyPathParams } from './routeConfig'
|
|
2
|
-
import { MatchLocation } from './router'
|
|
3
1
|
import { last } from './utils'
|
|
2
|
+
import type { MatchLocation } from './RouterProvider'
|
|
3
|
+
import type { AnyPathParams } from './route'
|
|
4
4
|
|
|
5
5
|
export interface Segment {
|
|
6
6
|
type: 'pathname' | 'param' | 'wildcard'
|
|
7
7
|
value: string
|
|
8
|
+
// Add a new property to store the static segment if present
|
|
9
|
+
prefixSegment?: string
|
|
10
|
+
suffixSegment?: string
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
export function joinPaths(paths:
|
|
11
|
-
return cleanPath(
|
|
13
|
+
export function joinPaths(paths: Array<string | undefined>) {
|
|
14
|
+
return cleanPath(
|
|
15
|
+
paths
|
|
16
|
+
.filter((val) => {
|
|
17
|
+
return val !== undefined
|
|
18
|
+
})
|
|
19
|
+
.join('/'),
|
|
20
|
+
)
|
|
12
21
|
}
|
|
13
22
|
|
|
14
23
|
export function cleanPath(path: string) {
|
|
@@ -28,13 +37,79 @@ export function trimPath(path: string) {
|
|
|
28
37
|
return trimPathRight(trimPathLeft(path))
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
export function removeTrailingSlash(value: string, basepath: string): string {
|
|
41
|
+
if (value?.endsWith('/') && value !== '/' && value !== `${basepath}/`) {
|
|
42
|
+
return value.slice(0, -1)
|
|
43
|
+
}
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// intended to only compare path name
|
|
48
|
+
// see the usage in the isActive under useLinkProps
|
|
49
|
+
// /sample/path1 = /sample/path1/
|
|
50
|
+
// /sample/path1/some <> /sample/path1
|
|
51
|
+
export function exactPathTest(
|
|
52
|
+
pathName1: string,
|
|
53
|
+
pathName2: string,
|
|
54
|
+
basepath: string,
|
|
55
|
+
): boolean {
|
|
56
|
+
return (
|
|
57
|
+
removeTrailingSlash(pathName1, basepath) ===
|
|
58
|
+
removeTrailingSlash(pathName2, basepath)
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// When resolving relative paths, we treat all paths as if they are trailing slash
|
|
63
|
+
// documents. All trailing slashes are removed after the path is resolved.
|
|
64
|
+
// Here are a few examples:
|
|
65
|
+
//
|
|
66
|
+
// /a/b/c + ./d = /a/b/c/d
|
|
67
|
+
// /a/b/c + ../d = /a/b/d
|
|
68
|
+
// /a/b/c + ./d/ = /a/b/c/d
|
|
69
|
+
// /a/b/c + ../d/ = /a/b/d
|
|
70
|
+
// /a/b/c + ./ = /a/b/c
|
|
71
|
+
//
|
|
72
|
+
// Absolute paths that start with `/` short circuit the resolution process to the root
|
|
73
|
+
// path.
|
|
74
|
+
//
|
|
75
|
+
// Here are some examples:
|
|
76
|
+
//
|
|
77
|
+
// /a/b/c + /d = /d
|
|
78
|
+
// /a/b/c + /d/ = /d
|
|
79
|
+
// /a/b/c + / = /
|
|
80
|
+
//
|
|
81
|
+
// Non-.-prefixed paths are still treated as relative paths, resolved like `./`
|
|
82
|
+
//
|
|
83
|
+
// Here are some examples:
|
|
84
|
+
//
|
|
85
|
+
// /a/b/c + d = /a/b/c/d
|
|
86
|
+
// /a/b/c + d/ = /a/b/c/d
|
|
87
|
+
// /a/b/c + d/e = /a/b/c/d/e
|
|
88
|
+
interface ResolvePathOptions {
|
|
89
|
+
basepath: string
|
|
90
|
+
base: string
|
|
91
|
+
to: string
|
|
92
|
+
trailingSlash?: 'always' | 'never' | 'preserve'
|
|
93
|
+
caseSensitive?: boolean
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolvePath({
|
|
97
|
+
basepath,
|
|
98
|
+
base,
|
|
99
|
+
to,
|
|
100
|
+
trailingSlash = 'never',
|
|
101
|
+
caseSensitive,
|
|
102
|
+
}: ResolvePathOptions) {
|
|
103
|
+
base = removeBasepath(basepath, base, caseSensitive)
|
|
104
|
+
to = removeBasepath(basepath, to, caseSensitive)
|
|
34
105
|
|
|
35
106
|
let baseSegments = parsePathname(base)
|
|
36
107
|
const toSegments = parsePathname(to)
|
|
37
108
|
|
|
109
|
+
if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
|
|
110
|
+
baseSegments.pop()
|
|
111
|
+
}
|
|
112
|
+
|
|
38
113
|
toSegments.forEach((toSegment, index) => {
|
|
39
114
|
if (toSegment.value === '/') {
|
|
40
115
|
if (!index) {
|
|
@@ -47,31 +122,78 @@ export function resolvePath(basepath: string, base: string, to: string) {
|
|
|
47
122
|
// ignore inter-slashes
|
|
48
123
|
}
|
|
49
124
|
} else if (toSegment.value === '..') {
|
|
50
|
-
// Extra trailing slash? pop it off
|
|
51
|
-
if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
|
|
52
|
-
baseSegments.pop()
|
|
53
|
-
}
|
|
54
125
|
baseSegments.pop()
|
|
55
126
|
} else if (toSegment.value === '.') {
|
|
56
|
-
|
|
127
|
+
// ignore
|
|
57
128
|
} else {
|
|
58
129
|
baseSegments.push(toSegment)
|
|
59
130
|
}
|
|
60
131
|
})
|
|
61
132
|
|
|
62
|
-
|
|
133
|
+
if (baseSegments.length > 1) {
|
|
134
|
+
if (last(baseSegments)?.value === '/') {
|
|
135
|
+
if (trailingSlash === 'never') {
|
|
136
|
+
baseSegments.pop()
|
|
137
|
+
}
|
|
138
|
+
} else if (trailingSlash === 'always') {
|
|
139
|
+
baseSegments.push({ type: 'pathname', value: '/' })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
63
142
|
|
|
143
|
+
const segmentValues = baseSegments.map((segment) => {
|
|
144
|
+
if (segment.type === 'param') {
|
|
145
|
+
const param = segment.value.substring(1)
|
|
146
|
+
if (segment.prefixSegment && segment.suffixSegment) {
|
|
147
|
+
return `${segment.prefixSegment}{$${param}}${segment.suffixSegment}`
|
|
148
|
+
} else if (segment.prefixSegment) {
|
|
149
|
+
return `${segment.prefixSegment}{$${param}}`
|
|
150
|
+
} else if (segment.suffixSegment) {
|
|
151
|
+
return `{$${param}}${segment.suffixSegment}`
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (segment.type === 'wildcard') {
|
|
155
|
+
if (segment.prefixSegment && segment.suffixSegment) {
|
|
156
|
+
return `${segment.prefixSegment}{$}${segment.suffixSegment}`
|
|
157
|
+
} else if (segment.prefixSegment) {
|
|
158
|
+
return `${segment.prefixSegment}{$}`
|
|
159
|
+
} else if (segment.suffixSegment) {
|
|
160
|
+
return `{$}${segment.suffixSegment}`
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return segment.value
|
|
164
|
+
})
|
|
165
|
+
const joined = joinPaths([basepath, ...segmentValues])
|
|
64
166
|
return cleanPath(joined)
|
|
65
167
|
}
|
|
66
168
|
|
|
67
|
-
|
|
169
|
+
const PARAM_RE = /^\$.{1,}$/ // $paramName
|
|
170
|
+
const PARAM_W_CURLY_BRACES_RE = /^(.*?)\{(\$[a-zA-Z_$][a-zA-Z0-9_$]*)\}(.*)$/ // prefix{$paramName}suffix
|
|
171
|
+
const WILDCARD_RE = /^\$$/ // $
|
|
172
|
+
const WILDCARD_W_CURLY_BRACES_RE = /^(.*?)\{\$\}(.*)$/ // prefix{$}suffix
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Required: `/foo/$bar` ✅
|
|
176
|
+
* Prefix and Suffix: `/foo/prefix${bar}suffix` ✅
|
|
177
|
+
* Wildcard: `/foo/$` ✅
|
|
178
|
+
* Wildcard with Prefix and Suffix: `/foo/prefix{$}suffix` ✅
|
|
179
|
+
*
|
|
180
|
+
* Future:
|
|
181
|
+
* Optional: `/foo/{-bar}`
|
|
182
|
+
* Optional named segment: `/foo/{bar}`
|
|
183
|
+
* Optional named segment with Prefix and Suffix: `/foo/prefix{-bar}suffix`
|
|
184
|
+
* Escape special characters:
|
|
185
|
+
* - `/foo/[$]` - Static route
|
|
186
|
+
* - `/foo/[$]{$foo} - Dynamic route with a static prefix of `$`
|
|
187
|
+
* - `/foo/{$foo}[$]` - Dynamic route with a static suffix of `$`
|
|
188
|
+
*/
|
|
189
|
+
export function parsePathname(pathname?: string): Array<Segment> {
|
|
68
190
|
if (!pathname) {
|
|
69
191
|
return []
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
pathname = cleanPath(pathname)
|
|
73
195
|
|
|
74
|
-
const segments: Segment
|
|
196
|
+
const segments: Array<Segment> = []
|
|
75
197
|
|
|
76
198
|
if (pathname.slice(0, 1) === '/') {
|
|
77
199
|
pathname = pathname.substring(1)
|
|
@@ -90,23 +212,63 @@ export function parsePathname(pathname?: string): Segment[] {
|
|
|
90
212
|
|
|
91
213
|
segments.push(
|
|
92
214
|
...split.map((part): Segment => {
|
|
93
|
-
|
|
215
|
+
// Check for wildcard with curly braces: prefix{$}suffix
|
|
216
|
+
const wildcardBracesMatch = part.match(WILDCARD_W_CURLY_BRACES_RE)
|
|
217
|
+
if (wildcardBracesMatch) {
|
|
218
|
+
const prefix = wildcardBracesMatch[1]
|
|
219
|
+
const suffix = wildcardBracesMatch[2]
|
|
94
220
|
return {
|
|
95
221
|
type: 'wildcard',
|
|
96
|
-
value:
|
|
222
|
+
value: '$',
|
|
223
|
+
prefixSegment: prefix || undefined,
|
|
224
|
+
suffixSegment: suffix || undefined,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check for the new parameter format: prefix{$paramName}suffix
|
|
229
|
+
const paramBracesMatch = part.match(PARAM_W_CURLY_BRACES_RE)
|
|
230
|
+
if (paramBracesMatch) {
|
|
231
|
+
const prefix = paramBracesMatch[1]
|
|
232
|
+
const paramName = paramBracesMatch[2]
|
|
233
|
+
const suffix = paramBracesMatch[3]
|
|
234
|
+
return {
|
|
235
|
+
type: 'param',
|
|
236
|
+
value: '' + paramName,
|
|
237
|
+
prefixSegment: prefix || undefined,
|
|
238
|
+
suffixSegment: suffix || undefined,
|
|
97
239
|
}
|
|
98
240
|
}
|
|
99
241
|
|
|
100
|
-
|
|
242
|
+
// Check for bare parameter format: $paramName (without curly braces)
|
|
243
|
+
if (PARAM_RE.test(part)) {
|
|
244
|
+
const paramName = part.substring(1)
|
|
101
245
|
return {
|
|
102
246
|
type: 'param',
|
|
103
|
-
value:
|
|
247
|
+
value: '$' + paramName,
|
|
248
|
+
prefixSegment: undefined,
|
|
249
|
+
suffixSegment: undefined,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for bare wildcard: $ (without curly braces)
|
|
254
|
+
if (WILDCARD_RE.test(part)) {
|
|
255
|
+
return {
|
|
256
|
+
type: 'wildcard',
|
|
257
|
+
value: '$',
|
|
258
|
+
prefixSegment: undefined,
|
|
259
|
+
suffixSegment: undefined,
|
|
104
260
|
}
|
|
105
261
|
}
|
|
106
262
|
|
|
263
|
+
// Handle regular pathname segment
|
|
107
264
|
return {
|
|
108
265
|
type: 'pathname',
|
|
109
|
-
value: part
|
|
266
|
+
value: part.includes('%25')
|
|
267
|
+
? part
|
|
268
|
+
.split('%25')
|
|
269
|
+
.map((segment) => decodeURI(segment))
|
|
270
|
+
.join('%25')
|
|
271
|
+
: decodeURI(part),
|
|
110
272
|
}
|
|
111
273
|
}),
|
|
112
274
|
)
|
|
@@ -122,56 +284,186 @@ export function parsePathname(pathname?: string): Segment[] {
|
|
|
122
284
|
return segments
|
|
123
285
|
}
|
|
124
286
|
|
|
125
|
-
|
|
126
|
-
path
|
|
127
|
-
params:
|
|
128
|
-
|
|
129
|
-
|
|
287
|
+
interface InterpolatePathOptions {
|
|
288
|
+
path?: string
|
|
289
|
+
params: Record<string, unknown>
|
|
290
|
+
leaveWildcards?: boolean
|
|
291
|
+
leaveParams?: boolean
|
|
292
|
+
// Map of encoded chars to decoded chars (e.g. '%40' -> '@') that should remain decoded in path params
|
|
293
|
+
decodeCharMap?: Map<string, string>
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
type InterPolatePathResult = {
|
|
297
|
+
interpolatedPath: string
|
|
298
|
+
usedParams: Record<string, unknown>
|
|
299
|
+
isMissingParams: boolean // true if any params were not available when being looked up in the params object
|
|
300
|
+
}
|
|
301
|
+
export function interpolatePath({
|
|
302
|
+
path,
|
|
303
|
+
params,
|
|
304
|
+
leaveWildcards,
|
|
305
|
+
leaveParams,
|
|
306
|
+
decodeCharMap,
|
|
307
|
+
}: InterpolatePathOptions): InterPolatePathResult {
|
|
130
308
|
const interpolatedPathSegments = parsePathname(path)
|
|
131
309
|
|
|
132
|
-
|
|
310
|
+
function encodeParam(key: string): any {
|
|
311
|
+
const value = params[key]
|
|
312
|
+
const isValueString = typeof value === 'string'
|
|
313
|
+
|
|
314
|
+
if (['*', '_splat'].includes(key)) {
|
|
315
|
+
// the splat/catch-all routes shouldn't have the '/' encoded out
|
|
316
|
+
return isValueString ? encodeURI(value) : value
|
|
317
|
+
} else {
|
|
318
|
+
return isValueString ? encodePathParam(value, decodeCharMap) : value
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Tracking if any params are missing in the `params` object
|
|
323
|
+
// when interpolating the path
|
|
324
|
+
let isMissingParams = false
|
|
325
|
+
|
|
326
|
+
const usedParams: Record<string, unknown> = {}
|
|
327
|
+
const interpolatedPath = joinPaths(
|
|
133
328
|
interpolatedPathSegments.map((segment) => {
|
|
134
|
-
if (segment.
|
|
135
|
-
|
|
329
|
+
if (segment.type === 'wildcard') {
|
|
330
|
+
usedParams._splat = params._splat
|
|
331
|
+
const segmentPrefix = segment.prefixSegment || ''
|
|
332
|
+
const segmentSuffix = segment.suffixSegment || ''
|
|
333
|
+
const value = encodeParam('_splat')
|
|
334
|
+
if (leaveWildcards) {
|
|
335
|
+
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
|
|
336
|
+
}
|
|
337
|
+
return `${segmentPrefix}${value}${segmentSuffix}`
|
|
136
338
|
}
|
|
137
339
|
|
|
138
340
|
if (segment.type === 'param') {
|
|
139
|
-
|
|
341
|
+
const key = segment.value.substring(1)
|
|
342
|
+
if (!isMissingParams && !(key in params)) {
|
|
343
|
+
isMissingParams = true
|
|
344
|
+
}
|
|
345
|
+
usedParams[key] = params[key]
|
|
346
|
+
|
|
347
|
+
const segmentPrefix = segment.prefixSegment || ''
|
|
348
|
+
const segmentSuffix = segment.suffixSegment || ''
|
|
349
|
+
if (leaveParams) {
|
|
350
|
+
const value = encodeParam(segment.value)
|
|
351
|
+
return `${segmentPrefix}${segment.value}${value ?? ''}${segmentSuffix}`
|
|
352
|
+
}
|
|
353
|
+
return `${segmentPrefix}${encodeParam(key) ?? 'undefined'}${segmentSuffix}`
|
|
140
354
|
}
|
|
141
355
|
|
|
142
356
|
return segment.value
|
|
143
357
|
}),
|
|
144
358
|
)
|
|
359
|
+
return { usedParams, interpolatedPath, isMissingParams }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
|
|
363
|
+
let encoded = encodeURIComponent(value)
|
|
364
|
+
if (decodeCharMap) {
|
|
365
|
+
for (const [encodedChar, char] of decodeCharMap) {
|
|
366
|
+
encoded = encoded.replaceAll(encodedChar, char)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return encoded
|
|
145
370
|
}
|
|
146
371
|
|
|
147
372
|
export function matchPathname(
|
|
373
|
+
basepath: string,
|
|
148
374
|
currentPathname: string,
|
|
149
375
|
matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
|
|
150
376
|
): AnyPathParams | undefined {
|
|
151
|
-
const pathParams = matchByPath(currentPathname, matchLocation)
|
|
152
|
-
// const searchMatched = matchBySearch(
|
|
377
|
+
const pathParams = matchByPath(basepath, currentPathname, matchLocation)
|
|
378
|
+
// const searchMatched = matchBySearch(location.search, matchLocation)
|
|
153
379
|
|
|
154
380
|
if (matchLocation.to && !pathParams) {
|
|
155
381
|
return
|
|
156
382
|
}
|
|
157
383
|
|
|
158
|
-
// if (matchLocation.search && !searchMatched) {
|
|
159
|
-
// return
|
|
160
|
-
// }
|
|
161
|
-
|
|
162
384
|
return pathParams ?? {}
|
|
163
385
|
}
|
|
164
386
|
|
|
387
|
+
export function removeBasepath(
|
|
388
|
+
basepath: string,
|
|
389
|
+
pathname: string,
|
|
390
|
+
caseSensitive: boolean = false,
|
|
391
|
+
) {
|
|
392
|
+
// normalize basepath and pathname for case-insensitive comparison if needed
|
|
393
|
+
const normalizedBasepath = caseSensitive ? basepath : basepath.toLowerCase()
|
|
394
|
+
const normalizedPathname = caseSensitive ? pathname : pathname.toLowerCase()
|
|
395
|
+
|
|
396
|
+
switch (true) {
|
|
397
|
+
// default behaviour is to serve app from the root - pathname
|
|
398
|
+
// left untouched
|
|
399
|
+
case normalizedBasepath === '/':
|
|
400
|
+
return pathname
|
|
401
|
+
|
|
402
|
+
// shortcut for removing the basepath if it matches the pathname
|
|
403
|
+
case normalizedPathname === normalizedBasepath:
|
|
404
|
+
return ''
|
|
405
|
+
|
|
406
|
+
// in case pathname is shorter than basepath - there is
|
|
407
|
+
// nothing to remove
|
|
408
|
+
case pathname.length < basepath.length:
|
|
409
|
+
return pathname
|
|
410
|
+
|
|
411
|
+
// avoid matching partial segments - strict equality handled
|
|
412
|
+
// earlier, otherwise, basepath separated from pathname with
|
|
413
|
+
// separator, therefore lack of separator means partial
|
|
414
|
+
// segment match (`/app` should not match `/application`)
|
|
415
|
+
case normalizedPathname[normalizedBasepath.length] !== '/':
|
|
416
|
+
return pathname
|
|
417
|
+
|
|
418
|
+
// remove the basepath from the pathname if it starts with it
|
|
419
|
+
case normalizedPathname.startsWith(normalizedBasepath):
|
|
420
|
+
return pathname.slice(basepath.length)
|
|
421
|
+
|
|
422
|
+
// otherwise, return the pathname as is
|
|
423
|
+
default:
|
|
424
|
+
return pathname
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
165
428
|
export function matchByPath(
|
|
429
|
+
basepath: string,
|
|
166
430
|
from: string,
|
|
167
431
|
matchLocation: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
|
|
168
432
|
): Record<string, string> | undefined {
|
|
433
|
+
// check basepath first
|
|
434
|
+
if (basepath !== '/' && !from.startsWith(basepath)) {
|
|
435
|
+
return undefined
|
|
436
|
+
}
|
|
437
|
+
// Remove the base path from the pathname
|
|
438
|
+
from = removeBasepath(basepath, from, matchLocation.caseSensitive)
|
|
439
|
+
// Default to to $ (wildcard)
|
|
440
|
+
const to = removeBasepath(
|
|
441
|
+
basepath,
|
|
442
|
+
`${matchLocation.to ?? '$'}`,
|
|
443
|
+
matchLocation.caseSensitive,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
// Parse the from and to
|
|
169
447
|
const baseSegments = parsePathname(from)
|
|
170
|
-
const routeSegments = parsePathname(
|
|
448
|
+
const routeSegments = parsePathname(to)
|
|
449
|
+
|
|
450
|
+
if (!from.startsWith('/')) {
|
|
451
|
+
baseSegments.unshift({
|
|
452
|
+
type: 'pathname',
|
|
453
|
+
value: '/',
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!to.startsWith('/')) {
|
|
458
|
+
routeSegments.unshift({
|
|
459
|
+
type: 'pathname',
|
|
460
|
+
value: '/',
|
|
461
|
+
})
|
|
462
|
+
}
|
|
171
463
|
|
|
172
464
|
const params: Record<string, string> = {}
|
|
173
465
|
|
|
174
|
-
|
|
466
|
+
const isMatch = (() => {
|
|
175
467
|
for (
|
|
176
468
|
let i = 0;
|
|
177
469
|
i < Math.max(baseSegments.length, routeSegments.length);
|
|
@@ -180,16 +472,66 @@ export function matchByPath(
|
|
|
180
472
|
const baseSegment = baseSegments[i]
|
|
181
473
|
const routeSegment = routeSegments[i]
|
|
182
474
|
|
|
183
|
-
const
|
|
184
|
-
const
|
|
475
|
+
const isLastBaseSegment = i >= baseSegments.length - 1
|
|
476
|
+
const isLastRouteSegment = i >= routeSegments.length - 1
|
|
185
477
|
|
|
186
478
|
if (routeSegment) {
|
|
187
479
|
if (routeSegment.type === 'wildcard') {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
480
|
+
// Capture all remaining segments for a wildcard
|
|
481
|
+
const remainingBaseSegments = baseSegments.slice(i)
|
|
482
|
+
|
|
483
|
+
let _splat: string
|
|
484
|
+
|
|
485
|
+
// If this is a wildcard with prefix/suffix, we need to handle the first segment specially
|
|
486
|
+
if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
|
|
487
|
+
if (!baseSegment) return false
|
|
488
|
+
|
|
489
|
+
const prefix = routeSegment.prefixSegment || ''
|
|
490
|
+
const suffix = routeSegment.suffixSegment || ''
|
|
491
|
+
|
|
492
|
+
// Check if the base segment starts with prefix and ends with suffix
|
|
493
|
+
const baseValue = baseSegment.value
|
|
494
|
+
if ('prefixSegment' in routeSegment) {
|
|
495
|
+
if (!baseValue.startsWith(prefix)) {
|
|
496
|
+
return false
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if ('suffixSegment' in routeSegment) {
|
|
500
|
+
if (
|
|
501
|
+
!baseSegments[baseSegments.length - 1]?.value.endsWith(suffix)
|
|
502
|
+
) {
|
|
503
|
+
return false
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let rejoinedSplat = decodeURI(
|
|
508
|
+
joinPaths(remainingBaseSegments.map((d) => d.value)),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
// Remove the prefix and suffix from the rejoined splat
|
|
512
|
+
if (prefix && rejoinedSplat.startsWith(prefix)) {
|
|
513
|
+
rejoinedSplat = rejoinedSplat.slice(prefix.length)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (suffix && rejoinedSplat.endsWith(suffix)) {
|
|
517
|
+
rejoinedSplat = rejoinedSplat.slice(
|
|
518
|
+
0,
|
|
519
|
+
rejoinedSplat.length - suffix.length,
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_splat = rejoinedSplat
|
|
524
|
+
} else {
|
|
525
|
+
// If no prefix/suffix, just rejoin the remaining segments
|
|
526
|
+
_splat = decodeURI(
|
|
527
|
+
joinPaths(remainingBaseSegments.map((d) => d.value)),
|
|
528
|
+
)
|
|
191
529
|
}
|
|
192
|
-
|
|
530
|
+
|
|
531
|
+
// TODO: Deprecate *
|
|
532
|
+
params['*'] = _splat
|
|
533
|
+
params['_splat'] = _splat
|
|
534
|
+
return true
|
|
193
535
|
}
|
|
194
536
|
|
|
195
537
|
if (routeSegment.type === 'pathname') {
|
|
@@ -216,21 +558,55 @@ export function matchByPath(
|
|
|
216
558
|
}
|
|
217
559
|
|
|
218
560
|
if (routeSegment.type === 'param') {
|
|
219
|
-
if (baseSegment
|
|
561
|
+
if (baseSegment.value === '/') {
|
|
220
562
|
return false
|
|
221
563
|
}
|
|
222
|
-
|
|
223
|
-
|
|
564
|
+
|
|
565
|
+
let _paramValue: string
|
|
566
|
+
|
|
567
|
+
// If this param has prefix/suffix, we need to extract the actual parameter value
|
|
568
|
+
if (routeSegment.prefixSegment || routeSegment.suffixSegment) {
|
|
569
|
+
const prefix = routeSegment.prefixSegment || ''
|
|
570
|
+
const suffix = routeSegment.suffixSegment || ''
|
|
571
|
+
|
|
572
|
+
// Check if the base segment starts with prefix and ends with suffix
|
|
573
|
+
const baseValue = baseSegment.value
|
|
574
|
+
if (prefix && !baseValue.startsWith(prefix)) {
|
|
575
|
+
return false
|
|
576
|
+
}
|
|
577
|
+
if (suffix && !baseValue.endsWith(suffix)) {
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let paramValue = baseValue
|
|
582
|
+
if (prefix && paramValue.startsWith(prefix)) {
|
|
583
|
+
paramValue = paramValue.slice(prefix.length)
|
|
584
|
+
}
|
|
585
|
+
if (suffix && paramValue.endsWith(suffix)) {
|
|
586
|
+
paramValue = paramValue.slice(
|
|
587
|
+
0,
|
|
588
|
+
paramValue.length - suffix.length,
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
_paramValue = decodeURIComponent(paramValue)
|
|
593
|
+
} else {
|
|
594
|
+
// If no prefix/suffix, just decode the base segment value
|
|
595
|
+
_paramValue = decodeURIComponent(baseSegment.value)
|
|
224
596
|
}
|
|
597
|
+
|
|
598
|
+
params[routeSegment.value.substring(1)] = _paramValue
|
|
225
599
|
}
|
|
226
600
|
}
|
|
227
601
|
|
|
228
|
-
if (
|
|
229
|
-
|
|
602
|
+
if (!isLastBaseSegment && isLastRouteSegment) {
|
|
603
|
+
params['**'] = joinPaths(baseSegments.slice(i + 1).map((d) => d.value))
|
|
604
|
+
return !!matchLocation.fuzzy && routeSegment?.value !== '/'
|
|
230
605
|
}
|
|
231
606
|
}
|
|
607
|
+
|
|
232
608
|
return true
|
|
233
609
|
})()
|
|
234
610
|
|
|
235
|
-
return isMatch ?
|
|
611
|
+
return isMatch ? params : undefined
|
|
236
612
|
}
|