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