-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
140 lines (122 loc) · 4.15 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
// TODO: Support match [...rest] style paths (we don't use them currently)
const nextStylePathComponent = /\[[^/]+\]/g
type Route<RouteName> = {
name: RouteName
identifiers: string[]
path: string
regex: RegExp
}
class NextNamedRoutes<RouteName> {
/**
* All registered routes.
*/
routes: Route<RouteName>[] = []
/**
* Registers a named route. The path should be the filesystem path corresponding to the route.
*/
add(name: RouteName, path: string): NextNamedRoutes<RouteName> {
if (this.findRouteByName(name)) {
throw new Error(`Duplicate route added: ${name}`)
}
this.routes.push({
name,
identifiers: NextNamedRoutes.identifiersInPath(path),
path,
regex: NextNamedRoutes.pathToRegex(path),
})
return this
}
/**
* Returns the currently active route (based on window.location)
* Uses regex matching internally
*/
activeRoute(): Route<RouteName> {
const rn = this.findRouteByPath(window.location.pathname)
if (!rn) {
throw new Error(`No route associated with this pathname: ${window.location.pathname}`)
}
return rn
}
/**
* Returns metadata about a route given its name.
* @param name The name of the route you want to find.
*/
findRouteByName(name: RouteName): Route<RouteName> | undefined {
return this.routes.find((route) => route.name === name)
}
/**
* Returns metadata about a route, based on a browser path.
* Uses regex matching internally.
* @param path The path to search for.
*/
findRouteByPath(path: string): Route<RouteName> | undefined {
return this.routes.find((route) => route.regex.test(path))
}
/**
* Returns the pathname (what is visible in URL bar) for the given route and parameters
* @param name The route to render.
* @param params The parameters that should be injected into the route
*/
pathnameForParams(name: RouteName, params: { [key: string]: string }): string {
const route = this.findRouteByName(name)
if (!route) {
throw new Error(`Could not find route: ${name}`)
}
return NextNamedRoutes.injectParamsIntoPath(route.path, params)
}
static injectParamsIntoPath(nextStylePath: string, params: { [key: string]: string }): string {
const identifiers = NextNamedRoutes.identifiersInPath(nextStylePath)
const pathname = identifiers.reduce((prev, curr) => {
return prev.replace(`[${curr}]`, params[curr])
}, nextStylePath)
// Add any search params to the route
const searchParams = Object.keys(params).filter((key) => !identifiers.includes(key))
let searchParamString = ''
if (searchParams.length > 0) {
searchParams.forEach((key) => {
if (params[key]) {
if (searchParamString.length === 0) {
searchParamString = '?'
} else if (searchParamString.length > 1) {
searchParamString += '&'
}
searchParamString += encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
}
})
}
return pathname + searchParamString
}
static pathToRegex(nextStylePath: string): RegExp {
const escapedPath = nextStylePath.replace(/[-{}()*+?.,\\^$|#\s]/g, '\\$&')
const identifiers = NextNamedRoutes.identifiersInPath(escapedPath)
// Create a regex using the identifiers
return new RegExp(
'^' +
identifiers.reduce((prev, curr) => {
return prev.replace(`[${curr}]`, '(?:([^/]+?))')
}, nextStylePath) +
'$',
)
}
static identifiersInPath(nextStylePath: string): string[] {
const matches = []
// string.matchAll is not standard enough yet :(
nextStylePathComponent.lastIndex = 0
let m = nextStylePathComponent.exec(nextStylePath)
while (m) {
matches.push(m[0])
m = nextStylePathComponent.exec(nextStylePath)
}
// Check for duplicates before returning, better be safe than sorry
const seen: { [key: string]: boolean } = {}
return matches.map((m) => {
const id = m.substr(1, m.length - 2)
if (seen[id]) {
throw new Error(`Duplicate identifier in path ${nextStylePath}: ${id}`)
}
seen[id] = true
return id
})
}
}
export default NextNamedRoutes