1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | import {
|
11 | match,
|
12 | Path,
|
13 | Match,
|
14 | MatchFunction,
|
15 | ParseOptions,
|
16 | TokensToRegexpOptions,
|
17 | RegexpToFunctionOptions,
|
18 | } from 'path-to-regexp'
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | export interface RouteParams {
|
24 | [paramName: string]: string | string[]
|
25 | }
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export interface RouterContext {
|
32 | [propName: string]: any
|
33 | }
|
34 |
|
35 | export interface ResolveContext extends RouterContext {
|
36 | |
37 |
|
38 |
|
39 | pathname: string
|
40 | }
|
41 |
|
42 | export interface RouteContext<R = any, C extends RouterContext = RouterContext>
|
43 | extends ResolveContext {
|
44 | |
45 |
|
46 |
|
47 | router: UniversalRouter<R, C>
|
48 | |
49 |
|
50 |
|
51 | route: Route<R, C>
|
52 | |
53 |
|
54 |
|
55 | baseUrl: string
|
56 | |
57 |
|
58 |
|
59 | path: string
|
60 | |
61 |
|
62 |
|
63 | params: RouteParams
|
64 | |
65 |
|
66 |
|
67 | next: (resume?: boolean) => Promise<R>
|
68 | }
|
69 |
|
70 | export type RouteResult<T> = T | Promise<T | null | undefined> | null | undefined
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | export interface Route<R = any, C extends RouterContext = RouterContext> {
|
80 | |
81 |
|
82 |
|
83 | path?: Path
|
84 | |
85 |
|
86 |
|
87 | name?: string
|
88 | |
89 |
|
90 |
|
91 | parent?: Route<R, C> | null
|
92 | |
93 |
|
94 |
|
95 | children?: Routes<R, C> | null
|
96 | |
97 |
|
98 |
|
99 |
|
100 | action?: (context: RouteContext<R, C>, params: RouteParams) => RouteResult<R>
|
101 | |
102 |
|
103 |
|
104 | match?: MatchFunction<RouteParams>
|
105 | }
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | export type Routes<R = any, C extends RouterContext = RouterContext> = Array<Route<R, C>>
|
114 |
|
115 | export type ResolveRoute<R = any, C extends RouterContext = RouterContext> = (
|
116 | context: RouteContext<R, C>,
|
117 | params: RouteParams,
|
118 | ) => RouteResult<R>
|
119 |
|
120 | export type RouteError = Error & { status?: number }
|
121 |
|
122 | export type ErrorHandler<R = any> = (error: RouteError, context: ResolveContext) => RouteResult<R>
|
123 |
|
124 | export interface RouterOptions<R = any, C extends RouterContext = RouterContext>
|
125 | extends ParseOptions,
|
126 | TokensToRegexpOptions,
|
127 | RegexpToFunctionOptions {
|
128 | context?: C
|
129 | baseUrl?: string
|
130 | resolveRoute?: ResolveRoute<R, C>
|
131 | errorHandler?: ErrorHandler<R>
|
132 | }
|
133 |
|
134 | export interface RouteMatch<R = any, C extends RouterContext = RouterContext> {
|
135 | route: Route<R, C>
|
136 | baseUrl: string
|
137 | path: string
|
138 | params: RouteParams
|
139 | }
|
140 |
|
141 | function decode(val: string): string {
|
142 | try {
|
143 | return decodeURIComponent(val)
|
144 | } catch (err) {
|
145 | return val
|
146 | }
|
147 | }
|
148 |
|
149 | function matchRoute<R, C>(
|
150 | route: Route<R, C>,
|
151 | baseUrl: string,
|
152 | options: RouterOptions<R, C>,
|
153 | pathname: string,
|
154 | parentParams?: RouteParams,
|
155 | ): Iterator<RouteMatch<R, C>, false, Route<R, C> | false> {
|
156 | let matchResult: Match<RouteParams>
|
157 | let childMatches: Iterator<RouteMatch<R, C>, false, Route<R, C> | false> | null
|
158 | let childIndex = 0
|
159 |
|
160 | return {
|
161 | next(routeToSkip: Route<R, C> | false): IteratorResult<RouteMatch<R, C>, false> {
|
162 | if (route === routeToSkip) {
|
163 | return { done: true, value: false }
|
164 | }
|
165 |
|
166 | if (!matchResult) {
|
167 | const rt = route
|
168 | const end = !rt.children
|
169 | if (!rt.match) {
|
170 | rt.match = match<RouteParams>(rt.path || '', { end, ...options })
|
171 | }
|
172 | matchResult = rt.match(pathname)
|
173 |
|
174 | if (matchResult) {
|
175 | const { path } = matchResult
|
176 | matchResult.path = !end && path.charAt(path.length - 1) === '/' ? path.substr(1) : path
|
177 | matchResult.params = { ...parentParams, ...matchResult.params }
|
178 | return {
|
179 | done: false,
|
180 | value: {
|
181 | route,
|
182 | baseUrl,
|
183 | path: matchResult.path,
|
184 | params: matchResult.params,
|
185 | },
|
186 | }
|
187 | }
|
188 | }
|
189 |
|
190 | if (matchResult && route.children) {
|
191 | while (childIndex < route.children.length) {
|
192 | if (!childMatches) {
|
193 | const childRoute = route.children[childIndex]
|
194 | childRoute.parent = route
|
195 |
|
196 | childMatches = matchRoute<R, C>(
|
197 | childRoute,
|
198 | baseUrl + matchResult.path,
|
199 | options,
|
200 | pathname.substr(matchResult.path.length),
|
201 | matchResult.params,
|
202 | )
|
203 | }
|
204 |
|
205 | const childMatch = childMatches.next(routeToSkip)
|
206 | if (!childMatch.done) {
|
207 | return {
|
208 | done: false,
|
209 | value: childMatch.value,
|
210 | }
|
211 | }
|
212 |
|
213 | childMatches = null
|
214 | childIndex++
|
215 | }
|
216 | }
|
217 |
|
218 | return { done: true, value: false }
|
219 | },
|
220 | }
|
221 | }
|
222 |
|
223 | function resolveRoute<R = any, C extends RouterContext = object>(
|
224 | context: RouteContext<R, C>,
|
225 | params: RouteParams,
|
226 | ): RouteResult<R> {
|
227 | if (typeof context.route.action === 'function') {
|
228 | return context.route.action(context, params)
|
229 | }
|
230 | return undefined
|
231 | }
|
232 |
|
233 | function isChildRoute<R = any, C extends RouterContext = object>(
|
234 | parentRoute: Route<R, C> | false,
|
235 | childRoute: Route<R, C>,
|
236 | ): boolean {
|
237 | let route: Route<R, C> | null | undefined = childRoute
|
238 | while (route) {
|
239 | route = route.parent
|
240 | if (route === parentRoute) {
|
241 | return true
|
242 | }
|
243 | }
|
244 | return false
|
245 | }
|
246 |
|
247 | class UniversalRouter<R = any, C extends RouterContext = RouterContext> {
|
248 | root: Route<R, C>
|
249 |
|
250 | baseUrl: string
|
251 |
|
252 | options: RouterOptions<R, C>
|
253 |
|
254 | constructor(routes: Routes<R, C> | Route<R, C>, options?: RouterOptions<R, C>) {
|
255 | if (!routes || typeof routes !== 'object') {
|
256 | throw new TypeError('Invalid routes')
|
257 | }
|
258 |
|
259 | this.options = { decode, ...options }
|
260 | this.baseUrl = this.options.baseUrl || ''
|
261 | this.root = Array.isArray(routes) ? { path: '', children: routes, parent: null } : routes
|
262 | this.root.parent = null
|
263 | }
|
264 |
|
265 | |
266 |
|
267 |
|
268 |
|
269 |
|
270 | resolve(pathnameOrContext: string | ResolveContext): RouteResult<R> {
|
271 | const context: ResolveContext = {
|
272 | router: this,
|
273 | ...this.options.context,
|
274 | ...(typeof pathnameOrContext === 'string'
|
275 | ? { pathname: pathnameOrContext }
|
276 | : pathnameOrContext),
|
277 | }
|
278 | const matchResult = matchRoute(
|
279 | this.root,
|
280 | this.baseUrl,
|
281 | this.options,
|
282 | context.pathname.substr(this.baseUrl.length),
|
283 | )
|
284 | const resolve = this.options.resolveRoute || resolveRoute
|
285 | let matches: IteratorResult<RouteMatch<R, C>, false>
|
286 | let nextMatches: IteratorResult<RouteMatch<R, C>, false> | null
|
287 | let currentContext = context
|
288 |
|
289 | function next(
|
290 | resume: boolean,
|
291 | parent: Route<R, C> | false = !matches.done && matches.value.route,
|
292 | prevResult?: RouteResult<R>,
|
293 | ): RouteResult<R> {
|
294 | const routeToSkip = prevResult === null && !matches.done && matches.value.route
|
295 | matches = nextMatches || matchResult.next(routeToSkip)
|
296 | nextMatches = null
|
297 |
|
298 | if (!resume) {
|
299 | if (matches.done || !isChildRoute(parent, matches.value.route)) {
|
300 | nextMatches = matches
|
301 | return Promise.resolve(null)
|
302 | }
|
303 | }
|
304 |
|
305 | if (matches.done) {
|
306 | const error: RouteError = new Error('Route not found')
|
307 | error.status = 404
|
308 | return Promise.reject(error)
|
309 | }
|
310 |
|
311 | currentContext = { ...context, ...matches.value }
|
312 |
|
313 | return Promise.resolve(
|
314 | resolve(currentContext as RouteContext<R, C>, matches.value.params),
|
315 | ).then((result) => {
|
316 | if (result !== null && result !== undefined) {
|
317 | return result
|
318 | }
|
319 | return next(resume, parent, result)
|
320 | })
|
321 | }
|
322 |
|
323 | context.next = next
|
324 |
|
325 | return Promise.resolve()
|
326 | .then(() => next(true, this.root))
|
327 | .catch((error) => {
|
328 | if (this.options.errorHandler) {
|
329 | return this.options.errorHandler(error, currentContext)
|
330 | }
|
331 | throw error
|
332 | })
|
333 | }
|
334 | }
|
335 |
|
336 | export default UniversalRouter
|