@ditojs/router 2.27.0 → 2.28.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/package.json +3 -3
- package/src/Node.js +9 -7
- package/src/Router.test.js +108 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dito.js Router – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
|
|
6
6
|
"repository": "https://github.com/ditojs/dito/tree/master/packages/router",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"node >= 18"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ditojs/utils": "^2.
|
|
24
|
+
"@ditojs/utils": "^2.28.0"
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"route",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"koa",
|
|
31
31
|
"dito"
|
|
32
32
|
],
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "19a0a2b37e1b5d2089079b6d624912f2a3f03f60",
|
|
34
34
|
"readme": "# Dito.js Router\n\nDito.js is a declarative and modern web framework with a focus on API driven\ndevelopment, based on Koa.js, Objection.js and Vue.js\n\nReleased in 2018 under the MIT license, with support by https://lineto.com/\n\nDito.js Router is a high performance, tree-based, framework agnostic HTTP\nrouter, based on [trek-router](https://github.com/trekjs/router), which in turn\nis inspired by [Echo](https://github.com/labstack/echo)'s Router.\n\n## How does it work?\n\nThe router relies on a tree structure which makes heavy use of common\nprefixes, essentially a [prefix tree](https://en.wikipedia.org/wiki/Trie).\n\n## Usage\n\n```js\nimport Koa from 'koa'\nimport Router from '@ditojs/router'\n\nconst app = new Koa()\nconst router = new Router()\n\n// static route\nrouter.get('/folders/files/bolt.gif', ctx => {\n ctx.body = `this ain't no GIF!`\n})\n\n// param route\nrouter.get('/users/:id', ctx => {\n ctx.body = `requesting user ${ctx.params.id}`\n})\n\n// match-any route\nrouter.get('/books/*', ctx => {\n ctx.body = `sub-route: ${ctx.params['*']}`\n})\n\n// Handler found\nlet { handler, params } = router.find('get', '/users/233')\nconsole.log(handler) // ctx => { ... }\n\n// Entry not Found\nlet { handler, params } = router.find('get', '/photos/233')\nconsole.log(handler) // null\n\n// Install router middleware\napp.use(async (ctx, next) => {\n const { method, path } = ctx\n const result = router.find(method, path)\n const { handler, params } = result\n if (handler) {\n ctx.params = params || {}\n return handler(ctx, next)\n } else {\n try {\n await next()\n } finally {\n if (ctx.body === undefined && ctx.status === 404) {\n ctx.status = result.status || 404\n if (ctx.status !== 404 && result.allowed) {\n ctx.set('Allow', result.allowed.join(', '))\n }\n }\n }\n }\n})\n\napp.listen(4040, () => console.log('Koa app listening on 4040'))\n```\n"
|
|
35
35
|
}
|
package/src/Node.js
CHANGED
|
@@ -26,11 +26,13 @@ export default class Node {
|
|
|
26
26
|
this.type = type
|
|
27
27
|
this.label = prefix[0]
|
|
28
28
|
this.prefix = prefix
|
|
29
|
-
this.children =
|
|
29
|
+
this.children = []
|
|
30
30
|
this.parameters = parameters
|
|
31
31
|
this.handler = handler
|
|
32
32
|
this.paramKey = null
|
|
33
|
-
|
|
33
|
+
for (const child of children) {
|
|
34
|
+
this.addChild(child)
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
setup(parameters, handler) {
|
|
@@ -65,7 +67,7 @@ export default class Node {
|
|
|
65
67
|
const start = pos
|
|
66
68
|
// Move pos to the next occurrence of the slash or the end:
|
|
67
69
|
pos += path.slice(pos).match(/^([^/]*)/)[1].length
|
|
68
|
-
parameters.
|
|
70
|
+
parameters.addKey(
|
|
69
71
|
isWildcard ? parameters.getPlaceholderKey() : path.slice(start, pos)
|
|
70
72
|
)
|
|
71
73
|
// Chop out param name from path, but keep colon.
|
|
@@ -240,7 +242,7 @@ class Parameters {
|
|
|
240
242
|
this.placeholderIndex = 0
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
|
|
245
|
+
addKey(key) {
|
|
244
246
|
this.keys.push(key)
|
|
245
247
|
}
|
|
246
248
|
|
|
@@ -269,15 +271,15 @@ class Parameters {
|
|
|
269
271
|
if (token === '**') {
|
|
270
272
|
const key = this.getMatchAnyKey()
|
|
271
273
|
pattern.push(`(?<${key}>.+?)`)
|
|
272
|
-
this.
|
|
274
|
+
this.addKey(key)
|
|
273
275
|
} else if (token === '*') {
|
|
274
276
|
const key = this.getPlaceholderKey()
|
|
275
277
|
pattern.push(`(?<${key}>[^/]+)`)
|
|
276
|
-
this.
|
|
278
|
+
this.addKey(key)
|
|
277
279
|
} else if (token.startsWith(':')) {
|
|
278
280
|
const key = token.slice(1)
|
|
279
281
|
pattern.push(`(?<${key}>[^/]+)`)
|
|
280
|
-
this.
|
|
282
|
+
this.addKey(key)
|
|
281
283
|
} else {
|
|
282
284
|
pattern.push(token)
|
|
283
285
|
}
|
package/src/Router.test.js
CHANGED
|
@@ -179,22 +179,27 @@ describe('Router', () => {
|
|
|
179
179
|
expect(result.params).toEqual({
|
|
180
180
|
$: 'hello'
|
|
181
181
|
})
|
|
182
|
+
|
|
183
|
+
result = router.find('GET', '/prefix/hello/there/suffix')
|
|
184
|
+
expect(result.handler).toBeUndefined()
|
|
182
185
|
})
|
|
183
186
|
|
|
184
187
|
it('handles match-any nodes with suffixes', () => {
|
|
185
|
-
router
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
createRoutes(router, [
|
|
189
|
+
['GET', '/static/**/suffix', 'one'],
|
|
190
|
+
['GET', '/static/**/suffix/**/more', 'two'],
|
|
191
|
+
['GET', '/static/**/suffix/*/1/:param/2', 'three'],
|
|
192
|
+
['GET', '/static/**/suffix/*/1/:param/2/**/end', 'four']
|
|
193
|
+
])
|
|
189
194
|
|
|
190
195
|
expect(router.toString()).toBe(
|
|
191
196
|
deindent`
|
|
192
197
|
/ children=1
|
|
193
198
|
└── static/ children=4
|
|
194
|
-
├── **/suffix
|
|
195
|
-
├── **/suffix/**/more
|
|
196
|
-
├── **/suffix/*/1/:param/2
|
|
197
|
-
└── **/suffix/*/1/:param/2/**/end
|
|
199
|
+
├── **/suffix one() children=0
|
|
200
|
+
├── **/suffix/**/more two() children=0
|
|
201
|
+
├── **/suffix/*/1/:param/2 three() children=0
|
|
202
|
+
└── **/suffix/*/1/:param/2/**/end four() children=0
|
|
198
203
|
`.trim()
|
|
199
204
|
)
|
|
200
205
|
|
|
@@ -211,13 +216,13 @@ describe('Router', () => {
|
|
|
211
216
|
expect(result.handler).toBeUndefined()
|
|
212
217
|
|
|
213
218
|
result = router.find('GET', '/static/one/suffix')
|
|
214
|
-
expect(result.handler).toBe(
|
|
219
|
+
expect(result.handler?.name).toBe('one')
|
|
215
220
|
expect(result.params).toEqual({
|
|
216
221
|
$$: 'one'
|
|
217
222
|
})
|
|
218
223
|
|
|
219
224
|
result = router.find('GET', '/static/one/two/suffix')
|
|
220
|
-
expect(result.handler).toBe(
|
|
225
|
+
expect(result.handler?.name).toBe('one')
|
|
221
226
|
expect(result.params).toEqual({
|
|
222
227
|
$$: 'one/two'
|
|
223
228
|
})
|
|
@@ -226,14 +231,14 @@ describe('Router', () => {
|
|
|
226
231
|
expect(result.handler).toBeUndefined()
|
|
227
232
|
|
|
228
233
|
result = router.find('GET', '/static/one/two/suffix/three/four/more')
|
|
229
|
-
expect(result.handler).toBe(
|
|
234
|
+
expect(result.handler?.name).toBe('two')
|
|
230
235
|
expect(result.params).toEqual({
|
|
231
236
|
$$0: 'one/two',
|
|
232
237
|
$$1: 'three/four'
|
|
233
238
|
})
|
|
234
239
|
|
|
235
240
|
result = router.find('GET', '/static/one/two/suffix/three/1/bla/2')
|
|
236
|
-
expect(result.handler).toBe(
|
|
241
|
+
expect(result.handler?.name).toBe('three')
|
|
237
242
|
expect(result.params).toEqual({
|
|
238
243
|
$$: 'one/two',
|
|
239
244
|
$: 'three',
|
|
@@ -247,7 +252,7 @@ describe('Router', () => {
|
|
|
247
252
|
'GET',
|
|
248
253
|
`/static/one/two/suffix/three/1/bla/2/what/ever/end`
|
|
249
254
|
)
|
|
250
|
-
expect(result.handler).toBe(
|
|
255
|
+
expect(result.handler?.name).toBe('four')
|
|
251
256
|
expect(result.params).toEqual({
|
|
252
257
|
$: 'three',
|
|
253
258
|
param: 'bla',
|
|
@@ -256,22 +261,49 @@ describe('Router', () => {
|
|
|
256
261
|
})
|
|
257
262
|
})
|
|
258
263
|
|
|
264
|
+
it.only('Allow match-any child-routes to coexist with other child-routes', () => {
|
|
265
|
+
createRoutes(router, [
|
|
266
|
+
['POST', '/api/admin/pages'],
|
|
267
|
+
['POST', '/api/admin/pages/upload/items/**/file'],
|
|
268
|
+
['POST', '/api/admin/pages/upload/items/**/files'],
|
|
269
|
+
['POST', '/api/admin/pages/:id/child-pages']
|
|
270
|
+
])
|
|
271
|
+
expect(router.toString('POST')).toBe(
|
|
272
|
+
deindent`
|
|
273
|
+
/ children=1
|
|
274
|
+
└── api/admin/pages ƒ() children=1
|
|
275
|
+
└── / children=2
|
|
276
|
+
├── upload/items/ children=2
|
|
277
|
+
│ ├── **/file ƒ() children=0
|
|
278
|
+
│ └── **/files ƒ() children=0
|
|
279
|
+
└── :id children=1
|
|
280
|
+
└── /child-pages ƒ() children=0
|
|
281
|
+
`.trim()
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
result = router.find('POST', '/api/admin/pages/upload/items/1/file')
|
|
285
|
+
expect(result.handler).not.toBeUndefined()
|
|
286
|
+
|
|
287
|
+
result = router.find('POST', '/api/admin/pages/upload/items/1/2/file')
|
|
288
|
+
expect(result.handler).not.toBeUndefined()
|
|
289
|
+
})
|
|
290
|
+
|
|
259
291
|
it('handles resources', () => {
|
|
260
292
|
createRoutes(router, [
|
|
261
|
-
['/', 'root'],
|
|
262
|
-
['/geocoder', 'geocoder'],
|
|
263
|
-
['/geocoder/new', 'newGeocoder'],
|
|
264
|
-
['/geocoder/notify', 'notifyGeocoder'],
|
|
265
|
-
// ['/geocoder/nnn', 'nnnGeocoder'],
|
|
266
|
-
['/geocoder/edit', 'editGeocoder'],
|
|
267
|
-
['/geocoder/edit/email', 'editEmailGeocoder'],
|
|
268
|
-
['/geocoder/edit/:item', 'editItemGeocoder'],
|
|
269
|
-
['/geocoder/exchange', 'exchangeGeocoder'],
|
|
270
|
-
['/geocoder/exchange/email', 'exchangeEmailGeocoder'],
|
|
271
|
-
['/geocoder/exchange/:item', 'exchangeItemGeocoder'],
|
|
272
|
-
['/geocoder/:id/echo', 'echoGeocoder'],
|
|
273
|
-
['/geocoder/:action', 'actionGeocoder'],
|
|
274
|
-
['/geocoder/**', 'anyGeocoder']
|
|
293
|
+
['GET', '/', 'root'],
|
|
294
|
+
['GET', '/geocoder', 'geocoder'],
|
|
295
|
+
['GET', '/geocoder/new', 'newGeocoder'],
|
|
296
|
+
['GET', '/geocoder/notify', 'notifyGeocoder'],
|
|
297
|
+
// ['GET', '/geocoder/nnn', 'nnnGeocoder'],
|
|
298
|
+
['GET', '/geocoder/edit', 'editGeocoder'],
|
|
299
|
+
['GET', '/geocoder/edit/email', 'editEmailGeocoder'],
|
|
300
|
+
['GET', '/geocoder/edit/:item', 'editItemGeocoder'],
|
|
301
|
+
['GET', '/geocoder/exchange', 'exchangeGeocoder'],
|
|
302
|
+
['GET', '/geocoder/exchange/email', 'exchangeEmailGeocoder'],
|
|
303
|
+
['GET', '/geocoder/exchange/:item', 'exchangeItemGeocoder'],
|
|
304
|
+
['GET', '/geocoder/:id/echo', 'echoGeocoder'],
|
|
305
|
+
['GET', '/geocoder/:action', 'actionGeocoder'],
|
|
306
|
+
['GET', '/geocoder/**', 'anyGeocoder']
|
|
275
307
|
])
|
|
276
308
|
|
|
277
309
|
expect(router.toString()).toBe(
|
|
@@ -377,17 +409,17 @@ describe('Router', () => {
|
|
|
377
409
|
|
|
378
410
|
it('handles nested resources', () => {
|
|
379
411
|
createRoutes(router, [
|
|
380
|
-
['/users', 'users'],
|
|
381
|
-
['/users/new', 'newUser'],
|
|
382
|
-
['/users/noi', 'newUser'],
|
|
383
|
-
['/users/nei', 'newUser'],
|
|
384
|
-
['/users/:id', 'user'],
|
|
385
|
-
['/users/:id/edit', 'editUser'],
|
|
386
|
-
['/users/:id/:hello/:good/:bad/ddd', 'editUser'],
|
|
387
|
-
['/users/:id/:action', 'actionUser'],
|
|
388
|
-
['/users/:userId/photos/:id', 'photo'],
|
|
389
|
-
['/users/:userId/books/:id', 'book'],
|
|
390
|
-
['/users/**', 'anyUser']
|
|
412
|
+
['GET', '/users', 'users'],
|
|
413
|
+
['GET', '/users/new', 'newUser'],
|
|
414
|
+
['GET', '/users/noi', 'newUser'],
|
|
415
|
+
['GET', '/users/nei', 'newUser'],
|
|
416
|
+
['GET', '/users/:id', 'user'],
|
|
417
|
+
['GET', '/users/:id/edit', 'editUser'],
|
|
418
|
+
['GET', '/users/:id/:hello/:good/:bad/ddd', 'editUser'],
|
|
419
|
+
['GET', '/users/:id/:action', 'actionUser'],
|
|
420
|
+
['GET', '/users/:userId/photos/:id', 'photo'],
|
|
421
|
+
['GET', '/users/:userId/books/:id', 'book'],
|
|
422
|
+
['GET', '/users/**', 'anyUser']
|
|
391
423
|
])
|
|
392
424
|
|
|
393
425
|
expect(router.toString()).toBe(
|
|
@@ -479,27 +511,27 @@ describe('Router', () => {
|
|
|
479
511
|
|
|
480
512
|
describe('handles multiple resources', () => {
|
|
481
513
|
const routes = [
|
|
482
|
-
['/users', 'users'],
|
|
483
|
-
['/users/new', 'newUser'],
|
|
484
|
-
['/users/:id', 'user'],
|
|
485
|
-
['/users/:id/:action', 'actionUser'],
|
|
486
|
-
['/users/:id/edit', 'editUser'],
|
|
487
|
-
['/users/:id/change', 'changeUser'],
|
|
488
|
-
['/users/:id/event', 'eventUser'],
|
|
489
|
-
['/photos', 'photos'],
|
|
490
|
-
['/photos/new', 'newPhoto'],
|
|
491
|
-
['/photos/:id', 'photo'],
|
|
492
|
-
['/photos/:id/:action', 'actionPhoto'],
|
|
493
|
-
['/photos/:id/edit', 'editPhoto'],
|
|
494
|
-
['/photos/:id/change', 'changePhoto'],
|
|
495
|
-
['/photos/:id/event', 'eventPhoto'],
|
|
496
|
-
['/books', 'books'],
|
|
497
|
-
['/books/new', 'newBook'],
|
|
498
|
-
['/books/:id', 'book'],
|
|
499
|
-
['/books/:id/:action', 'actionBook'],
|
|
500
|
-
['/books/:id/edit', 'editBook'],
|
|
501
|
-
['/books/:id/change', 'changeBook'],
|
|
502
|
-
['/books/:id/event', 'eventBook']
|
|
514
|
+
['GET', '/users', 'users'],
|
|
515
|
+
['GET', '/users/new', 'newUser'],
|
|
516
|
+
['GET', '/users/:id', 'user'],
|
|
517
|
+
['GET', '/users/:id/:action', 'actionUser'],
|
|
518
|
+
['GET', '/users/:id/edit', 'editUser'],
|
|
519
|
+
['GET', '/users/:id/change', 'changeUser'],
|
|
520
|
+
['GET', '/users/:id/event', 'eventUser'],
|
|
521
|
+
['GET', '/photos', 'photos'],
|
|
522
|
+
['GET', '/photos/new', 'newPhoto'],
|
|
523
|
+
['GET', '/photos/:id', 'photo'],
|
|
524
|
+
['GET', '/photos/:id/:action', 'actionPhoto'],
|
|
525
|
+
['GET', '/photos/:id/edit', 'editPhoto'],
|
|
526
|
+
['GET', '/photos/:id/change', 'changePhoto'],
|
|
527
|
+
['GET', '/photos/:id/event', 'eventPhoto'],
|
|
528
|
+
['GET', '/books', 'books'],
|
|
529
|
+
['GET', '/books/new', 'newBook'],
|
|
530
|
+
['GET', '/books/:id', 'book'],
|
|
531
|
+
['GET', '/books/:id/:action', 'actionBook'],
|
|
532
|
+
['GET', '/books/:id/edit', 'editBook'],
|
|
533
|
+
['GET', '/books/:id/change', 'changeBook'],
|
|
534
|
+
['GET', '/books/:id/event', 'eventBook']
|
|
503
535
|
]
|
|
504
536
|
|
|
505
537
|
it('parses routes into the correct tree', () => {
|
|
@@ -632,10 +664,10 @@ describe('Router', () => {
|
|
|
632
664
|
|
|
633
665
|
describe('handles namespaces', () => {
|
|
634
666
|
const routes = [
|
|
635
|
-
['/admin/articles', 'articles'],
|
|
636
|
-
['/admin/articles/new', 'newArticle'],
|
|
637
|
-
['/admin/articles/:id', 'article'],
|
|
638
|
-
['/admin/articles/:id/edit', 'editArticle']
|
|
667
|
+
['GET', '/admin/articles', 'articles'],
|
|
668
|
+
['GET', '/admin/articles/new', 'newArticle'],
|
|
669
|
+
['GET', '/admin/articles/:id', 'article'],
|
|
670
|
+
['GET', '/admin/articles/:id/edit', 'editArticle']
|
|
639
671
|
]
|
|
640
672
|
|
|
641
673
|
it('parses routes into the correct tree', () => {
|
|
@@ -677,14 +709,14 @@ describe('Router', () => {
|
|
|
677
709
|
|
|
678
710
|
describe('handles multiple nested resources', () => {
|
|
679
711
|
const routes = [
|
|
680
|
-
['/magazines/:mid/articles', 'articles'],
|
|
681
|
-
['/magazines/:mid/articles/new', 'newArticle'],
|
|
682
|
-
['/magazines/:mid/articles/:id', 'article'],
|
|
683
|
-
['/magazines/:mid/articles/:id/edit', 'editArticle'],
|
|
684
|
-
['/magazines/:m_id/photos', 'photos'],
|
|
685
|
-
['/magazines/:m_id/photos/new', 'newPhoto'],
|
|
686
|
-
['/magazines/:m_id/photos/:id', 'photo'],
|
|
687
|
-
['/magazines/:m_id/photos/:id/edit', 'editPhoto']
|
|
712
|
+
['GET', '/magazines/:mid/articles', 'articles'],
|
|
713
|
+
['GET', '/magazines/:mid/articles/new', 'newArticle'],
|
|
714
|
+
['GET', '/magazines/:mid/articles/:id', 'article'],
|
|
715
|
+
['GET', '/magazines/:mid/articles/:id/edit', 'editArticle'],
|
|
716
|
+
['GET', '/magazines/:m_id/photos', 'photos'],
|
|
717
|
+
['GET', '/magazines/:m_id/photos/new', 'newPhoto'],
|
|
718
|
+
['GET', '/magazines/:m_id/photos/:id', 'photo'],
|
|
719
|
+
['GET', '/magazines/:m_id/photos/:id/edit', 'editPhoto']
|
|
688
720
|
]
|
|
689
721
|
|
|
690
722
|
it('parses routes into the correct tree', () => {
|
|
@@ -781,11 +813,13 @@ describe('Router', () => {
|
|
|
781
813
|
|
|
782
814
|
function createFunc(name) {
|
|
783
815
|
// eslint-disable-next-line no-new-func
|
|
784
|
-
return new Function(
|
|
816
|
+
return new Function(
|
|
817
|
+
`return ${name ? `function ${name}(){}` : '() => {}'}`
|
|
818
|
+
)()
|
|
785
819
|
}
|
|
786
820
|
|
|
787
821
|
function createRoutes(router, routes) {
|
|
788
|
-
routes.forEach(([path, name]) => {
|
|
789
|
-
router.add(
|
|
822
|
+
routes.forEach(([method, path, name]) => {
|
|
823
|
+
router.add(method, path, createFunc(name))
|
|
790
824
|
})
|
|
791
825
|
}
|