kdu-router 3.0.7 → 3.1.7

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.
@@ -10,9 +10,9 @@ export function createRouteMap (
10
10
  oldPathMap?: Dictionary<RouteRecord>,
11
11
  oldNameMap?: Dictionary<RouteRecord>
12
12
  ): {
13
- pathList: Array<string>;
14
- pathMap: Dictionary<RouteRecord>;
15
- nameMap: Dictionary<RouteRecord>;
13
+ pathList: Array<string>,
14
+ pathMap: Dictionary<RouteRecord>,
15
+ nameMap: Dictionary<RouteRecord>
16
16
  } {
17
17
  // the path list is used to control path matching priority
18
18
  const pathList: Array<string> = oldPathList || []
@@ -34,6 +34,18 @@ export function createRouteMap (
34
34
  }
35
35
  }
36
36
 
37
+ if (process.env.NODE_ENV === 'development') {
38
+ // warn if routes do not include leading slashes
39
+ const found = pathList
40
+ // check for missing leading slash
41
+ .filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
42
+
43
+ if (found.length > 0) {
44
+ const pathNames = found.map(path => `- ${path}`).join('\n')
45
+ warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
46
+ }
47
+ }
48
+
37
49
  return {
38
50
  pathList,
39
51
  pathMap,
@@ -54,17 +66,15 @@ function addRouteRecord (
54
66
  assert(path != null, `"path" is required in a route configuration.`)
55
67
  assert(
56
68
  typeof route.component !== 'string',
57
- `route config "component" for path: ${String(path || name)} cannot be a ` +
58
- `string id. Use an actual component instead.`
69
+ `route config "component" for path: ${String(
70
+ path || name
71
+ )} cannot be a ` + `string id. Use an actual component instead.`
59
72
  )
60
73
  }
61
74
 
62
- const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
63
- const normalizedPath = normalizePath(
64
- path,
65
- parent,
66
- pathToRegexpOptions.strict
67
- )
75
+ const pathToRegexpOptions: PathToRegexpOptions =
76
+ route.pathToRegexpOptions || {}
77
+ const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
68
78
 
69
79
  if (typeof route.caseSensitive === 'boolean') {
70
80
  pathToRegexpOptions.sensitive = route.caseSensitive
@@ -81,11 +91,12 @@ function addRouteRecord (
81
91
  redirect: route.redirect,
82
92
  beforeEnter: route.beforeEnter,
83
93
  meta: route.meta || {},
84
- props: route.props == null
85
- ? {}
86
- : route.components
87
- ? route.props
88
- : { default: route.props }
94
+ props:
95
+ route.props == null
96
+ ? {}
97
+ : route.components
98
+ ? route.props
99
+ : { default: route.props }
89
100
  }
90
101
 
91
102
  if (route.children) {
@@ -93,14 +104,20 @@ function addRouteRecord (
93
104
  // If users navigate to this route by name, the default child will
94
105
  // not be rendered (GH Issue #629)
95
106
  if (process.env.NODE_ENV !== 'production') {
96
- if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
107
+ if (
108
+ route.name &&
109
+ !route.redirect &&
110
+ route.children.some(child => /^\/?$/.test(child.path))
111
+ ) {
97
112
  warn(
98
113
  false,
99
114
  `Named Route '${route.name}' has a default child route. ` +
100
- `When navigating to this named route (:to="{name: '${route.name}'"), ` +
101
- `the default child route will not be rendered. Remove the name from ` +
102
- `this route and use the name of the default child route for named ` +
103
- `links instead.`
115
+ `When navigating to this named route (:to="{name: '${
116
+ route.name
117
+ }'"), ` +
118
+ `the default child route will not be rendered. Remove the name from ` +
119
+ `this route and use the name of the default child route for named ` +
120
+ `links instead.`
104
121
  )
105
122
  }
106
123
  }
@@ -112,12 +129,24 @@ function addRouteRecord (
112
129
  })
113
130
  }
114
131
 
132
+ if (!pathMap[record.path]) {
133
+ pathList.push(record.path)
134
+ pathMap[record.path] = record
135
+ }
136
+
115
137
  if (route.alias !== undefined) {
116
- const aliases = Array.isArray(route.alias)
117
- ? route.alias
118
- : [route.alias]
138
+ const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
139
+ for (let i = 0; i < aliases.length; ++i) {
140
+ const alias = aliases[i]
141
+ if (process.env.NODE_ENV !== 'production' && alias === path) {
142
+ warn(
143
+ false,
144
+ `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
145
+ )
146
+ // skip in dev to make it work
147
+ continue
148
+ }
119
149
 
120
- aliases.forEach(alias => {
121
150
  const aliasRoute = {
122
151
  path: alias,
123
152
  children: route.children
@@ -130,12 +159,7 @@ function addRouteRecord (
130
159
  parent,
131
160
  record.path || '/' // matchAs
132
161
  )
133
- })
134
- }
135
-
136
- if (!pathMap[record.path]) {
137
- pathList.push(record.path)
138
- pathMap[record.path] = record
162
+ }
139
163
  }
140
164
 
141
165
  if (name) {
@@ -145,25 +169,35 @@ function addRouteRecord (
145
169
  warn(
146
170
  false,
147
171
  `Duplicate named routes definition: ` +
148
- `{ name: "${name}", path: "${record.path}" }`
172
+ `{ name: "${name}", path: "${record.path}" }`
149
173
  )
150
174
  }
151
175
  }
152
176
  }
153
177
 
154
- function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp {
178
+ function compileRouteRegex (
179
+ path: string,
180
+ pathToRegexpOptions: PathToRegexpOptions
181
+ ): RouteRegExp {
155
182
  const regex = Regexp(path, [], pathToRegexpOptions)
156
183
  if (process.env.NODE_ENV !== 'production') {
157
184
  const keys: any = Object.create(null)
158
185
  regex.keys.forEach(key => {
159
- warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`)
186
+ warn(
187
+ !keys[key.name],
188
+ `Duplicate param keys in route with path: "${path}"`
189
+ )
160
190
  keys[key.name] = true
161
191
  })
162
192
  }
163
193
  return regex
164
194
  }
165
195
 
166
- function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string {
196
+ function normalizePath (
197
+ path: string,
198
+ parent?: RouteRecord,
199
+ strict?: boolean
200
+ ): string {
167
201
  if (!strict) path = path.replace(/\/$/, '')
168
202
  if (path[0] === '/') return path
169
203
  if (parent == null) return path
@@ -0,0 +1,69 @@
1
+ /* @flow */
2
+
3
+ import type Router from '../index'
4
+ import { History } from './base'
5
+ import { NavigationDuplicated } from './errors'
6
+ import { isExtendedError } from '../util/warn'
7
+
8
+ export class AbstractHistory extends History {
9
+ index: number
10
+ stack: Array<Route>
11
+
12
+ constructor (router: Router, base: ?string) {
13
+ super(router, base)
14
+ this.stack = []
15
+ this.index = -1
16
+ }
17
+
18
+ push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
19
+ this.transitionTo(
20
+ location,
21
+ route => {
22
+ this.stack = this.stack.slice(0, this.index + 1).concat(route)
23
+ this.index++
24
+ onComplete && onComplete(route)
25
+ },
26
+ onAbort
27
+ )
28
+ }
29
+
30
+ replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
31
+ this.transitionTo(
32
+ location,
33
+ route => {
34
+ this.stack = this.stack.slice(0, this.index).concat(route)
35
+ onComplete && onComplete(route)
36
+ },
37
+ onAbort
38
+ )
39
+ }
40
+
41
+ go (n: number) {
42
+ const targetIndex = this.index + n
43
+ if (targetIndex < 0 || targetIndex >= this.stack.length) {
44
+ return
45
+ }
46
+ const route = this.stack[targetIndex]
47
+ this.confirmTransition(
48
+ route,
49
+ () => {
50
+ this.index = targetIndex
51
+ this.updateRoute(route)
52
+ },
53
+ err => {
54
+ if (isExtendedError(NavigationDuplicated, err)) {
55
+ this.index = targetIndex
56
+ }
57
+ }
58
+ )
59
+ }
60
+
61
+ getCurrentLocation () {
62
+ const current = this.stack[this.stack.length - 1]
63
+ return current ? current.fullPath : '/'
64
+ }
65
+
66
+ ensureURL () {
67
+ // noop
68
+ }
69
+ }
@@ -0,0 +1,352 @@
1
+ /* @flow */
2
+
3
+ import { _Kdu } from '../install'
4
+ import type Router from '../index'
5
+ import { inBrowser } from '../util/dom'
6
+ import { runQueue } from '../util/async'
7
+ import { warn, isError, isExtendedError } from '../util/warn'
8
+ import { START, isSameRoute } from '../util/route'
9
+ import {
10
+ flatten,
11
+ flatMapComponents,
12
+ resolveAsyncComponents
13
+ } from '../util/resolve-components'
14
+ import { NavigationDuplicated } from './errors'
15
+
16
+ export class History {
17
+ router: Router
18
+ base: string
19
+ current: Route
20
+ pending: ?Route
21
+ cb: (r: Route) => void
22
+ ready: boolean
23
+ readyCbs: Array<Function>
24
+ readyErrorCbs: Array<Function>
25
+ errorCbs: Array<Function>
26
+
27
+ // implemented by sub-classes
28
+ +go: (n: number) => void
29
+ +push: (loc: RawLocation) => void
30
+ +replace: (loc: RawLocation) => void
31
+ +ensureURL: (push?: boolean) => void
32
+ +getCurrentLocation: () => string
33
+
34
+ constructor (router: Router, base: ?string) {
35
+ this.router = router
36
+ this.base = normalizeBase(base)
37
+ // start with a route object that stands for "nowhere"
38
+ this.current = START
39
+ this.pending = null
40
+ this.ready = false
41
+ this.readyCbs = []
42
+ this.readyErrorCbs = []
43
+ this.errorCbs = []
44
+ }
45
+
46
+ listen (cb: Function) {
47
+ this.cb = cb
48
+ }
49
+
50
+ onReady (cb: Function, errorCb: ?Function) {
51
+ if (this.ready) {
52
+ cb()
53
+ } else {
54
+ this.readyCbs.push(cb)
55
+ if (errorCb) {
56
+ this.readyErrorCbs.push(errorCb)
57
+ }
58
+ }
59
+ }
60
+
61
+ onError (errorCb: Function) {
62
+ this.errorCbs.push(errorCb)
63
+ }
64
+
65
+ transitionTo (
66
+ location: RawLocation,
67
+ onComplete?: Function,
68
+ onAbort?: Function
69
+ ) {
70
+ const route = this.router.match(location, this.current)
71
+ this.confirmTransition(
72
+ route,
73
+ () => {
74
+ this.updateRoute(route)
75
+ onComplete && onComplete(route)
76
+ this.ensureURL()
77
+
78
+ // fire ready cbs once
79
+ if (!this.ready) {
80
+ this.ready = true
81
+ this.readyCbs.forEach(cb => {
82
+ cb(route)
83
+ })
84
+ }
85
+ },
86
+ err => {
87
+ if (onAbort) {
88
+ onAbort(err)
89
+ }
90
+ if (err && !this.ready) {
91
+ this.ready = true
92
+ this.readyErrorCbs.forEach(cb => {
93
+ cb(err)
94
+ })
95
+ }
96
+ }
97
+ )
98
+ }
99
+
100
+ confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
101
+ const current = this.current
102
+ const abort = err => {
103
+ // When the user navigates through history through back/forward buttons
104
+ // we do not want to throw the error. We only throw it if directly calling
105
+ // push/replace. That's why it's not included in isError
106
+ if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
107
+ if (this.errorCbs.length) {
108
+ this.errorCbs.forEach(cb => {
109
+ cb(err)
110
+ })
111
+ } else {
112
+ warn(false, 'uncaught error during route navigation:')
113
+ console.error(err)
114
+ }
115
+ }
116
+ onAbort && onAbort(err)
117
+ }
118
+ if (
119
+ isSameRoute(route, current) &&
120
+ // in the case the route map has been dynamically appended to
121
+ route.matched.length === current.matched.length
122
+ ) {
123
+ this.ensureURL()
124
+ return abort(new NavigationDuplicated(route))
125
+ }
126
+
127
+ const { updated, deactivated, activated } = resolveQueue(
128
+ this.current.matched,
129
+ route.matched
130
+ )
131
+
132
+ const queue: Array<?NavigationGuard> = [].concat(
133
+ // in-component leave guards
134
+ extractLeaveGuards(deactivated),
135
+ // global before hooks
136
+ this.router.beforeHooks,
137
+ // in-component update hooks
138
+ extractUpdateHooks(updated),
139
+ // in-config enter guards
140
+ activated.map(m => m.beforeEnter),
141
+ // async components
142
+ resolveAsyncComponents(activated)
143
+ )
144
+
145
+ this.pending = route
146
+ const iterator = (hook: NavigationGuard, next) => {
147
+ if (this.pending !== route) {
148
+ return abort()
149
+ }
150
+ try {
151
+ hook(route, current, (to: any) => {
152
+ if (to === false || isError(to)) {
153
+ // next(false) -> abort navigation, ensure current URL
154
+ this.ensureURL(true)
155
+ abort(to)
156
+ } else if (
157
+ typeof to === 'string' ||
158
+ (typeof to === 'object' &&
159
+ (typeof to.path === 'string' || typeof to.name === 'string'))
160
+ ) {
161
+ // next('/') or next({ path: '/' }) -> redirect
162
+ abort()
163
+ if (typeof to === 'object' && to.replace) {
164
+ this.replace(to)
165
+ } else {
166
+ this.push(to)
167
+ }
168
+ } else {
169
+ // confirm transition and pass on the value
170
+ next(to)
171
+ }
172
+ })
173
+ } catch (e) {
174
+ abort(e)
175
+ }
176
+ }
177
+
178
+ runQueue(queue, iterator, () => {
179
+ const postEnterCbs = []
180
+ const isValid = () => this.current === route
181
+ // wait until async components are resolved before
182
+ // extracting in-component enter guards
183
+ const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
184
+ const queue = enterGuards.concat(this.router.resolveHooks)
185
+ runQueue(queue, iterator, () => {
186
+ if (this.pending !== route) {
187
+ return abort()
188
+ }
189
+ this.pending = null
190
+ onComplete(route)
191
+ if (this.router.app) {
192
+ this.router.app.$nextTick(() => {
193
+ postEnterCbs.forEach(cb => {
194
+ cb()
195
+ })
196
+ })
197
+ }
198
+ })
199
+ })
200
+ }
201
+
202
+ updateRoute (route: Route) {
203
+ const prev = this.current
204
+ this.current = route
205
+ this.cb && this.cb(route)
206
+ this.router.afterHooks.forEach(hook => {
207
+ hook && hook(route, prev)
208
+ })
209
+ }
210
+ }
211
+
212
+ function normalizeBase (base: ?string): string {
213
+ if (!base) {
214
+ if (inBrowser) {
215
+ // respect <base> tag
216
+ const baseEl = document.querySelector('base')
217
+ base = (baseEl && baseEl.getAttribute('href')) || '/'
218
+ // strip full URL origin
219
+ base = base.replace(/^https?:\/\/[^\/]+/, '')
220
+ } else {
221
+ base = '/'
222
+ }
223
+ }
224
+ // make sure there's the starting slash
225
+ if (base.charAt(0) !== '/') {
226
+ base = '/' + base
227
+ }
228
+ // remove trailing slash
229
+ return base.replace(/\/$/, '')
230
+ }
231
+
232
+ function resolveQueue (
233
+ current: Array<RouteRecord>,
234
+ next: Array<RouteRecord>
235
+ ): {
236
+ updated: Array<RouteRecord>,
237
+ activated: Array<RouteRecord>,
238
+ deactivated: Array<RouteRecord>
239
+ } {
240
+ let i
241
+ const max = Math.max(current.length, next.length)
242
+ for (i = 0; i < max; i++) {
243
+ if (current[i] !== next[i]) {
244
+ break
245
+ }
246
+ }
247
+ return {
248
+ updated: next.slice(0, i),
249
+ activated: next.slice(i),
250
+ deactivated: current.slice(i)
251
+ }
252
+ }
253
+
254
+ function extractGuards (
255
+ records: Array<RouteRecord>,
256
+ name: string,
257
+ bind: Function,
258
+ reverse?: boolean
259
+ ): Array<?Function> {
260
+ const guards = flatMapComponents(records, (def, instance, match, key) => {
261
+ const guard = extractGuard(def, name)
262
+ if (guard) {
263
+ return Array.isArray(guard)
264
+ ? guard.map(guard => bind(guard, instance, match, key))
265
+ : bind(guard, instance, match, key)
266
+ }
267
+ })
268
+ return flatten(reverse ? guards.reverse() : guards)
269
+ }
270
+
271
+ function extractGuard (
272
+ def: Object | Function,
273
+ key: string
274
+ ): NavigationGuard | Array<NavigationGuard> {
275
+ if (typeof def !== 'function') {
276
+ // extend now so that global mixins are applied.
277
+ def = _Kdu.extend(def)
278
+ }
279
+ return def.options[key]
280
+ }
281
+
282
+ function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
283
+ return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
284
+ }
285
+
286
+ function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
287
+ return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
288
+ }
289
+
290
+ function bindGuard (guard: NavigationGuard, instance: ?_Kdu): ?NavigationGuard {
291
+ if (instance) {
292
+ return function boundRouteGuard () {
293
+ return guard.apply(instance, arguments)
294
+ }
295
+ }
296
+ }
297
+
298
+ function extractEnterGuards (
299
+ activated: Array<RouteRecord>,
300
+ cbs: Array<Function>,
301
+ isValid: () => boolean
302
+ ): Array<?Function> {
303
+ return extractGuards(
304
+ activated,
305
+ 'beforeRouteEnter',
306
+ (guard, _, match, key) => {
307
+ return bindEnterGuard(guard, match, key, cbs, isValid)
308
+ }
309
+ )
310
+ }
311
+
312
+ function bindEnterGuard (
313
+ guard: NavigationGuard,
314
+ match: RouteRecord,
315
+ key: string,
316
+ cbs: Array<Function>,
317
+ isValid: () => boolean
318
+ ): NavigationGuard {
319
+ return function routeEnterGuard (to, from, next) {
320
+ return guard(to, from, cb => {
321
+ if (typeof cb === 'function') {
322
+ cbs.push(() => {
323
+ // #750
324
+ // if a router-view is wrapped with an out-in transition,
325
+ // the instance may not have been registered at this time.
326
+ // we will need to poll for registration until current route
327
+ // is no longer valid.
328
+ poll(cb, match.instances, key, isValid)
329
+ })
330
+ }
331
+ next(cb)
332
+ })
333
+ }
334
+ }
335
+
336
+ function poll (
337
+ cb: any, // somehow flow cannot infer this is a function
338
+ instances: Object,
339
+ key: string,
340
+ isValid: () => boolean
341
+ ) {
342
+ if (
343
+ instances[key] &&
344
+ !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
345
+ ) {
346
+ cb(instances[key])
347
+ } else if (isValid()) {
348
+ setTimeout(() => {
349
+ poll(cb, instances, key, isValid)
350
+ }, 16)
351
+ }
352
+ }
@@ -0,0 +1,22 @@
1
+ export class NavigationDuplicated extends Error {
2
+ constructor (normalizedLocation) {
3
+ super()
4
+ this.name = this._name = 'NavigationDuplicated'
5
+ // passing the message to super() doesn't seem to work in the transpiled version
6
+ this.message = `Navigating to current location ("${
7
+ normalizedLocation.fullPath
8
+ }") is not allowed`
9
+ // add a stack property so services like Sentry can correctly display it
10
+ Object.defineProperty(this, 'stack', {
11
+ value: new Error().stack,
12
+ writable: true,
13
+ configurable: true
14
+ })
15
+ // we could also have used
16
+ // Error.captureStackTrace(this, this.constructor)
17
+ // but it only exists on node and chrome
18
+ }
19
+ }
20
+
21
+ // support IE9
22
+ NavigationDuplicated._name = 'NavigationDuplicated'