hono 0.0.2 → 0.0.6
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/README.md +150 -9
- package/package.json +2 -2
- package/src/compose.js +22 -0
- package/src/compose.test.js +42 -0
- package/src/hono.js +113 -49
- package/src/hono.test.js +94 -9
- package/src/middleware/defaultFilter.js +19 -0
- package/src/node.js +97 -0
- package/src/node.test.js +135 -0
- package/src/router.test.js +73 -51
- package/src/util.js +26 -0
- package/src/util.test.js +29 -0
- package/src/router.js +0 -116
package/README.md
CHANGED
|
@@ -1,10 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hono
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hono [炎] - Tiny web framework for Cloudflare Workers and others.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const Hono = require('Hono')
|
|
7
|
+
const app = Hono()
|
|
8
|
+
|
|
9
|
+
app.get('/', () => new Response('Hono!!'))
|
|
10
|
+
|
|
11
|
+
app.fire()
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Feature
|
|
15
|
+
|
|
16
|
+
- Fast - the router is implemented with Trie-Tree structure.
|
|
17
|
+
- Tiny - use only standard API.
|
|
18
|
+
- Portable - zero dependencies.
|
|
19
|
+
- Flexible - you can make your own middlewares.
|
|
20
|
+
- Optimized - for Cloudflare Workers and Fastly Compute@Edge.
|
|
21
|
+
|
|
22
|
+
## Benchmark
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
hono x 813,001 ops/sec ±2.96% (75 runs sampled)
|
|
26
|
+
itty-router x 160,415 ops/sec ±3.31% (85 runs sampled)
|
|
27
|
+
sunder x 307,438 ops/sec ±4.77% (73 runs sampled)
|
|
28
|
+
Fastest is hono
|
|
29
|
+
✨ Done in 37.03s.
|
|
30
|
+
```
|
|
4
31
|
|
|
5
32
|
## Install
|
|
6
33
|
|
|
7
|
-
```
|
|
34
|
+
```
|
|
8
35
|
$ yarn add hono
|
|
9
36
|
```
|
|
10
37
|
|
|
@@ -14,18 +41,132 @@ or
|
|
|
14
41
|
$ npm install hono
|
|
15
42
|
```
|
|
16
43
|
|
|
17
|
-
##
|
|
44
|
+
## Methods
|
|
45
|
+
|
|
46
|
+
- app.**HTTP_METHOD**(path, callback)
|
|
47
|
+
- app.**all**(path, callback)
|
|
48
|
+
- app.**route**(path)
|
|
49
|
+
- app.**use**(path, middleware)
|
|
50
|
+
|
|
51
|
+
## Routing
|
|
52
|
+
|
|
53
|
+
### Basic
|
|
54
|
+
|
|
55
|
+
`app.HTTP_METHOD`
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// HTTP Methods
|
|
59
|
+
app.get('/', () => new Response('GET /'))
|
|
60
|
+
app.post('/', () => new Response('POST /'))
|
|
61
|
+
|
|
62
|
+
// Wildcard
|
|
63
|
+
app.get('/wild/*/card', () => {
|
|
64
|
+
return new Response('GET /wild/*/card')
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`app.all`
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
// Any HTTP methods
|
|
72
|
+
app.all('/hello', () => new Response('ALL Method /hello'))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Named Parameter
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
app.get('/user/:name', (c) => {
|
|
79
|
+
const name = c.req.params('name')
|
|
80
|
+
...
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Regexp
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
|
|
88
|
+
const date = c.req.params('date')
|
|
89
|
+
const title = c.req.params('title')
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Chained Route
|
|
18
94
|
|
|
19
95
|
```js
|
|
20
|
-
|
|
21
|
-
|
|
96
|
+
app
|
|
97
|
+
.route('/api/book')
|
|
98
|
+
.get(() => {...})
|
|
99
|
+
.post(() => {...})
|
|
100
|
+
.put(() => {...})
|
|
101
|
+
```
|
|
22
102
|
|
|
23
|
-
|
|
103
|
+
## Middleware
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const logger = (c, next) => {
|
|
107
|
+
console.log(`[${c.req.method}] ${c.req.url}`)
|
|
108
|
+
next()
|
|
109
|
+
}
|
|
24
110
|
|
|
25
|
-
|
|
111
|
+
const addHeader = (c, next) => {
|
|
112
|
+
next()
|
|
113
|
+
c.res.headers.add('x-message', 'This is middleware!')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
app = app.use('*', logger)
|
|
117
|
+
app = app.use('/message/*', addHeader)
|
|
118
|
+
|
|
119
|
+
app.get('/message/hello', () => 'Hello Middleware!')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Context
|
|
123
|
+
|
|
124
|
+
### req
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
app.get('/hello', (c) => {
|
|
128
|
+
const userAgent = c.req.headers('User-Agent')
|
|
129
|
+
...
|
|
130
|
+
})
|
|
26
131
|
```
|
|
27
132
|
|
|
28
|
-
|
|
133
|
+
### res
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
app.use('/', (c, next) => {
|
|
137
|
+
next()
|
|
138
|
+
c.res.headers.append('X-Debug', 'Debug message')
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Request
|
|
143
|
+
|
|
144
|
+
### query
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
app.get('/search', (c) => {
|
|
148
|
+
const query = c.req.query('q')
|
|
149
|
+
...
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### params
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
app.get('/entry/:id', (c) => {
|
|
157
|
+
const id = c.req.params('id')
|
|
158
|
+
...
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Related projects
|
|
163
|
+
|
|
164
|
+
- koa <https://github.com/koajs/koa>
|
|
165
|
+
- express <https://github.com/expressjs/express>
|
|
166
|
+
- oak <https://github.com/oakserver/oak>
|
|
167
|
+
- itty-router <https://github.com/kwhitley/itty-router>
|
|
168
|
+
- Sunder <https://github.com/SunderJS/sunder>
|
|
169
|
+
- goblin <https://github.com/bmf-san/goblin>
|
|
29
170
|
|
|
30
171
|
## Author
|
|
31
172
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Minimal web framework for Cloudflare Workers",
|
|
5
5
|
"main": "src/hono.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "jest
|
|
7
|
+
"test": "jest"
|
|
8
8
|
},
|
|
9
9
|
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
|
|
10
10
|
"license": "MIT",
|
package/src/compose.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Based on the code in the MIT licensed `koa-compose` package.
|
|
2
|
+
const compose = (middleware) => {
|
|
3
|
+
return function (context, next) {
|
|
4
|
+
let index = -1
|
|
5
|
+
return dispatch(0)
|
|
6
|
+
function dispatch(i) {
|
|
7
|
+
if (i <= index)
|
|
8
|
+
return Promise.reject(new Error('next() called multiple times'))
|
|
9
|
+
index = i
|
|
10
|
+
let fn = middleware[i]
|
|
11
|
+
if (i === middleware.length) fn = next
|
|
12
|
+
if (!fn) return Promise.resolve()
|
|
13
|
+
try {
|
|
14
|
+
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return Promise.reject(err)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = compose
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const compose = require('./compose')
|
|
2
|
+
|
|
3
|
+
describe('compose middleware', () => {
|
|
4
|
+
const middleware = []
|
|
5
|
+
|
|
6
|
+
const a = (c, next) => {
|
|
7
|
+
c.req['log'] = 'log'
|
|
8
|
+
next()
|
|
9
|
+
}
|
|
10
|
+
middleware.push(a)
|
|
11
|
+
|
|
12
|
+
const b = (c, next) => {
|
|
13
|
+
next()
|
|
14
|
+
c.res['header'] = `${c.res.header}-custom-header`
|
|
15
|
+
}
|
|
16
|
+
middleware.push(b)
|
|
17
|
+
|
|
18
|
+
const handler = (c, next) => {
|
|
19
|
+
c.req['log'] = `${c.req.log} message`
|
|
20
|
+
next()
|
|
21
|
+
c.res = { message: 'new response' }
|
|
22
|
+
}
|
|
23
|
+
middleware.push(handler)
|
|
24
|
+
|
|
25
|
+
const request = {}
|
|
26
|
+
const response = {}
|
|
27
|
+
|
|
28
|
+
it('Request', () => {
|
|
29
|
+
const c = { req: request, res: response }
|
|
30
|
+
const composed = compose(middleware)
|
|
31
|
+
composed(c)
|
|
32
|
+
expect(c.req['log']).not.toBeNull()
|
|
33
|
+
expect(c.req['log']).toBe('log message')
|
|
34
|
+
})
|
|
35
|
+
it('Response', () => {
|
|
36
|
+
const c = { req: request, res: response }
|
|
37
|
+
const composed = compose(middleware)
|
|
38
|
+
composed(c)
|
|
39
|
+
expect(c.res['header']).not.toBeNull()
|
|
40
|
+
expect(c.res['message']).toBe('new response')
|
|
41
|
+
})
|
|
42
|
+
})
|
package/src/hono.js
CHANGED
|
@@ -1,77 +1,141 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const Node = require('./node')
|
|
4
|
+
const compose = require('./compose')
|
|
5
|
+
const defaultFilter = require('./middleware/defaultFilter')
|
|
6
|
+
|
|
7
|
+
const METHOD_NAME_OF_ALL = 'ALL'
|
|
8
|
+
|
|
9
|
+
class Router {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.node = new Node()
|
|
12
|
+
this.tempPath = '/'
|
|
7
13
|
}
|
|
14
|
+
|
|
15
|
+
add(method, path, ...handlers) {
|
|
16
|
+
this.node.insert(method, path, handlers)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
match(method, path) {
|
|
20
|
+
return this.node.search(method, path)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getPathFromURL = (url) => {
|
|
25
|
+
// XXX
|
|
26
|
+
const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
|
|
27
|
+
return match[5]
|
|
8
28
|
}
|
|
9
29
|
|
|
10
|
-
|
|
30
|
+
const proxyHandler = {
|
|
31
|
+
get:
|
|
32
|
+
(target, prop) =>
|
|
33
|
+
(...args) => {
|
|
34
|
+
if (target.constructor.prototype.hasOwnProperty(prop)) {
|
|
35
|
+
return target[prop](...args)
|
|
36
|
+
} else {
|
|
37
|
+
return target.addRoute(prop, ...args)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Hono {
|
|
11
43
|
constructor() {
|
|
12
|
-
this.router = new Router()
|
|
44
|
+
this.router = new Router()
|
|
45
|
+
this.middlewareRouter = new Router()
|
|
46
|
+
this.middlewareRouters = []
|
|
13
47
|
}
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
this.router
|
|
49
|
+
getRouter() {
|
|
50
|
+
return this.router
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addRoute(method, ...args) {
|
|
54
|
+
method = method.toUpperCase()
|
|
55
|
+
if (args.length === 1) {
|
|
56
|
+
this.router.add(method, this.router.tempPath, ...args)
|
|
57
|
+
} else {
|
|
58
|
+
this.router.add(method, ...args)
|
|
59
|
+
}
|
|
60
|
+
return WrappedApp(this)
|
|
17
61
|
}
|
|
18
62
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
63
|
+
route(path) {
|
|
64
|
+
this.router.tempPath = path
|
|
65
|
+
return WrappedApp(this)
|
|
22
66
|
}
|
|
23
67
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
68
|
+
use(path, middleware) {
|
|
69
|
+
const router = new Router()
|
|
70
|
+
router.add(METHOD_NAME_OF_ALL, path, middleware)
|
|
71
|
+
this.middlewareRouters.push(router)
|
|
72
|
+
}
|
|
28
73
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
async matchRoute(method, path) {
|
|
75
|
+
return this.router.match(method, path)
|
|
76
|
+
}
|
|
32
77
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
// XXX
|
|
79
|
+
async createContext(req, res) {
|
|
80
|
+
return {
|
|
81
|
+
req: req,
|
|
82
|
+
res: res,
|
|
83
|
+
newResponse: (params) => {
|
|
84
|
+
return new Response(params)
|
|
85
|
+
},
|
|
38
86
|
}
|
|
39
|
-
return this.notFound()
|
|
40
87
|
}
|
|
41
88
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
async dispatch(request, response) {
|
|
90
|
+
const [method, path] = [request.method, getPathFromURL(request.url)]
|
|
91
|
+
|
|
92
|
+
const result = await this.matchRoute(method, path)
|
|
93
|
+
if (!result) return this.notFound()
|
|
94
|
+
|
|
95
|
+
request.params = (key) => result.params[key]
|
|
96
|
+
|
|
97
|
+
let handler = result.handler[0] // XXX
|
|
98
|
+
|
|
99
|
+
const middleware = [defaultFilter] // add defaultFilter later
|
|
100
|
+
|
|
101
|
+
for (const mr of this.middlewareRouters) {
|
|
102
|
+
const mwResult = mr.match(METHOD_NAME_OF_ALL, path)
|
|
103
|
+
if (mwResult) {
|
|
104
|
+
middleware.push(...mwResult.handler)
|
|
47
105
|
}
|
|
48
|
-
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let wrappedHandler = async (context, next) => {
|
|
109
|
+
context.res = handler(context)
|
|
110
|
+
next()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
middleware.push(wrappedHandler)
|
|
114
|
+
const composed = compose(middleware)
|
|
115
|
+
const c = await this.createContext(request, response)
|
|
116
|
+
|
|
117
|
+
composed(c)
|
|
118
|
+
|
|
119
|
+
return c.res
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async handleEvent(event) {
|
|
123
|
+
return this.dispatch(event.request, {}) // XXX
|
|
49
124
|
}
|
|
50
125
|
|
|
51
126
|
fire() {
|
|
52
|
-
addEventListener(
|
|
53
|
-
this.
|
|
127
|
+
addEventListener('fetch', (event) => {
|
|
128
|
+
event.respondWith(this.handleEvent(event))
|
|
54
129
|
})
|
|
55
130
|
}
|
|
56
|
-
}
|
|
57
131
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (target.constructor.prototype.hasOwnProperty(prop)) {
|
|
61
|
-
return target[prop](args[0])
|
|
62
|
-
} else {
|
|
63
|
-
target.addRoute(prop, args[0], args[1])
|
|
64
|
-
return
|
|
65
|
-
}
|
|
132
|
+
notFound() {
|
|
133
|
+
return new Response('Not Found', { status: 404 })
|
|
66
134
|
}
|
|
67
135
|
}
|
|
68
136
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
function Hono() {
|
|
72
|
-
return new Proxy(
|
|
73
|
-
app, proxyHandler
|
|
74
|
-
)
|
|
137
|
+
const WrappedApp = (hono = new Hono()) => {
|
|
138
|
+
return new Proxy(hono, proxyHandler)
|
|
75
139
|
}
|
|
76
140
|
|
|
77
|
-
module.exports =
|
|
141
|
+
module.exports = WrappedApp
|
package/src/hono.test.js
CHANGED
|
@@ -1,29 +1,114 @@
|
|
|
1
1
|
const Hono = require('./hono')
|
|
2
2
|
const fetch = require('node-fetch')
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
describe('GET match', () => {
|
|
4
|
+
describe('GET Request', () => {
|
|
5
|
+
const app = Hono()
|
|
7
6
|
app.get('/hello', () => {
|
|
8
7
|
return new fetch.Response('hello', {
|
|
9
|
-
status: 200
|
|
8
|
+
status: 200,
|
|
10
9
|
})
|
|
11
10
|
})
|
|
12
11
|
app.notFound = () => {
|
|
13
12
|
return new fetch.Response('not found', {
|
|
14
|
-
status: 404
|
|
13
|
+
status: 404,
|
|
15
14
|
})
|
|
16
15
|
}
|
|
17
|
-
it('GET /hello is ok', () => {
|
|
16
|
+
it('GET /hello is ok', async () => {
|
|
18
17
|
let req = new fetch.Request('https://example.com/hello')
|
|
19
|
-
let res = app.dispatch(req)
|
|
18
|
+
let res = await app.dispatch(req, new fetch.Response())
|
|
20
19
|
expect(res).not.toBeNull()
|
|
21
20
|
expect(res.status).toBe(200)
|
|
21
|
+
expect(await res.text()).toBe('hello')
|
|
22
22
|
})
|
|
23
|
-
it('GET / is not found', () => {
|
|
23
|
+
it('GET / is not found', async () => {
|
|
24
24
|
let req = new fetch.Request('https://example.com/')
|
|
25
|
-
let res = app.dispatch(req)
|
|
25
|
+
let res = await app.dispatch(req, new fetch.Response())
|
|
26
26
|
expect(res).not.toBeNull()
|
|
27
27
|
expect(res.status).toBe(404)
|
|
28
28
|
})
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
describe('params and query', () => {
|
|
32
|
+
const app = Hono()
|
|
33
|
+
|
|
34
|
+
app.get('/entry/:id', async (c) => {
|
|
35
|
+
const id = await c.req.params('id')
|
|
36
|
+
return new fetch.Response(`id is ${id}`, {
|
|
37
|
+
status: 200,
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
app.get('/search', async (c) => {
|
|
42
|
+
const name = await c.req.query('name')
|
|
43
|
+
return new fetch.Response(`name is ${name}`, {
|
|
44
|
+
status: 200,
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('params of /entry/:id is found', async () => {
|
|
49
|
+
let req = new fetch.Request('https://example.com/entry/123')
|
|
50
|
+
let res = await app.dispatch(req)
|
|
51
|
+
expect(res.status).toBe(200)
|
|
52
|
+
expect(await res.text()).toBe('id is 123')
|
|
53
|
+
})
|
|
54
|
+
it('query of /search?name=sam is found', async () => {
|
|
55
|
+
let req = new fetch.Request('https://example.com/search?name=sam')
|
|
56
|
+
let res = await app.dispatch(req)
|
|
57
|
+
expect(res.status).toBe(200)
|
|
58
|
+
expect(await res.text()).toBe('name is sam')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('Middleware', () => {
|
|
63
|
+
const app = Hono()
|
|
64
|
+
|
|
65
|
+
const logger = async (c, next) => {
|
|
66
|
+
console.log(`${c.req.method} : ${c.req.url}`)
|
|
67
|
+
next()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rootHeader = async (c, next) => {
|
|
71
|
+
next()
|
|
72
|
+
await c.res.headers.append('x-custom', 'root')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const customHeader = async (c, next) => {
|
|
76
|
+
next()
|
|
77
|
+
await c.res.headers.append('x-message', 'custom-header')
|
|
78
|
+
}
|
|
79
|
+
const customHeader2 = async (c, next) => {
|
|
80
|
+
next()
|
|
81
|
+
await c.res.headers.append('x-message-2', 'custom-header-2')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
app.use('*', logger)
|
|
85
|
+
app.use('*', rootHeader)
|
|
86
|
+
app.use('/hello', customHeader)
|
|
87
|
+
app.use('/hello/*', customHeader2)
|
|
88
|
+
app.get('/hello', () => {
|
|
89
|
+
return new fetch.Response('hello')
|
|
90
|
+
})
|
|
91
|
+
app.get('/hello/:message', (c) => {
|
|
92
|
+
const message = c.req.params('message')
|
|
93
|
+
return new fetch.Response(`${message}`)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('logging and custom header', async () => {
|
|
97
|
+
let req = new fetch.Request('https://example.com/hello')
|
|
98
|
+
let res = await app.dispatch(req)
|
|
99
|
+
expect(res.status).toBe(200)
|
|
100
|
+
expect(await res.text()).toBe('hello')
|
|
101
|
+
expect(await res.headers.get('x-custom')).toBe('root')
|
|
102
|
+
expect(await res.headers.get('x-message')).toBe('custom-header')
|
|
103
|
+
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('logging and custom header with named params', async () => {
|
|
107
|
+
let req = new fetch.Request('https://example.com/hello/message')
|
|
108
|
+
let res = await app.dispatch(req)
|
|
109
|
+
expect(res.status).toBe(200)
|
|
110
|
+
expect(await res.text()).toBe('message')
|
|
111
|
+
expect(await res.headers.get('x-custom')).toBe('root')
|
|
112
|
+
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const defaultFilter = (c, next) => {
|
|
2
|
+
c.req.query = (key) => {
|
|
3
|
+
const url = new URL(c.req.url)
|
|
4
|
+
return url.searchParams.get(key)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
next()
|
|
8
|
+
|
|
9
|
+
if (typeof c.res === 'string') {
|
|
10
|
+
c.res = new Reponse(c.res, {
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: {
|
|
13
|
+
'Conten-Type': 'text/plain',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = defaultFilter
|
package/src/node.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { splitPath, getPattern } = require('./util')
|
|
4
|
+
|
|
5
|
+
const METHOD_NAME_OF_ALL = 'ALL'
|
|
6
|
+
|
|
7
|
+
const createResult = (handler, params) => {
|
|
8
|
+
return { handler: handler, params: params }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const noRoute = () => {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Node(method, handler, children) {
|
|
16
|
+
this.children = children || {}
|
|
17
|
+
this.method = {}
|
|
18
|
+
if (method && handler) {
|
|
19
|
+
this.method[method] = handler
|
|
20
|
+
}
|
|
21
|
+
this.middlewares = []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Node.prototype.insert = function (method, path, handler) {
|
|
25
|
+
let curNode = this
|
|
26
|
+
const parts = splitPath(path)
|
|
27
|
+
for (let i = 0; i < parts.length; i++) {
|
|
28
|
+
const p = parts[i]
|
|
29
|
+
if (Object.keys(curNode.children).includes(p)) {
|
|
30
|
+
curNode = curNode.children[p]
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
curNode.children[p] = new Node(method, handler)
|
|
34
|
+
curNode = curNode.children[p]
|
|
35
|
+
}
|
|
36
|
+
curNode.method[method] = handler
|
|
37
|
+
return curNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Node.prototype.search = function (method, path) {
|
|
41
|
+
let curNode = this
|
|
42
|
+
|
|
43
|
+
const params = {}
|
|
44
|
+
const parts = splitPath(path)
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < parts.length; i++) {
|
|
47
|
+
const p = parts[i]
|
|
48
|
+
const nextNode = curNode.children[p]
|
|
49
|
+
if (nextNode) {
|
|
50
|
+
curNode = nextNode
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let isWildcard = false
|
|
55
|
+
let isParamMatch = false
|
|
56
|
+
const keys = Object.keys(curNode.children)
|
|
57
|
+
for (let j = 0; j < keys.length; j++) {
|
|
58
|
+
const key = keys[j]
|
|
59
|
+
// Wildcard
|
|
60
|
+
if (key === '*') {
|
|
61
|
+
curNode = curNode.children['*']
|
|
62
|
+
isWildcard = true
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
const pattern = getPattern(key)
|
|
66
|
+
if (pattern) {
|
|
67
|
+
const match = p.match(new RegExp(pattern[1]))
|
|
68
|
+
if (match) {
|
|
69
|
+
const k = pattern[0]
|
|
70
|
+
params[k] = match[1]
|
|
71
|
+
curNode = curNode.children[key]
|
|
72
|
+
isParamMatch = true
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
return noRoute()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isWildcard) {
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isParamMatch === false) {
|
|
84
|
+
return noRoute()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
|
|
89
|
+
|
|
90
|
+
if (!handler) {
|
|
91
|
+
return noRoute()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return createResult(handler, params)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = Node
|
package/src/node.test.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const Node = require('./node')
|
|
2
|
+
|
|
3
|
+
describe('Root Node', () => {
|
|
4
|
+
const node = new Node()
|
|
5
|
+
node.insert('get', '/', 'get root')
|
|
6
|
+
it('get /', () => {
|
|
7
|
+
let res = node.search('get', '/')
|
|
8
|
+
expect(res).not.toBeNull()
|
|
9
|
+
expect(res.handler).toBe('get root')
|
|
10
|
+
expect(node.search('get', '/hello')).toBeNull()
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('Root Node id not defined', () => {
|
|
15
|
+
const node = new Node()
|
|
16
|
+
node.insert('get', '/hello', 'get hello')
|
|
17
|
+
it('get /', () => {
|
|
18
|
+
expect(node.search('get', '/')).toBeNull()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('All with *', () => {
|
|
23
|
+
const node = new Node()
|
|
24
|
+
node.insert('get', '*', 'get all')
|
|
25
|
+
it('get /', () => {
|
|
26
|
+
expect(node.search('get', '/')).not.toBeNull()
|
|
27
|
+
expect(node.search('get', '/hello')).not.toBeNull()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('Basic Usage', () => {
|
|
32
|
+
const node = new Node()
|
|
33
|
+
node.insert('get', '/hello', 'get hello')
|
|
34
|
+
node.insert('post', '/hello', 'post hello')
|
|
35
|
+
node.insert('get', '/hello/foo', 'get hello foo')
|
|
36
|
+
|
|
37
|
+
it('get, post /hello', () => {
|
|
38
|
+
expect(node.search('get', '/')).toBeNull()
|
|
39
|
+
expect(node.search('post', '/')).toBeNull()
|
|
40
|
+
|
|
41
|
+
expect(node.search('get', '/hello').handler).toBe('get hello')
|
|
42
|
+
expect(node.search('post', '/hello').handler).toBe('post hello')
|
|
43
|
+
expect(node.search('put', '/hello')).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
it('get /nothing', () => {
|
|
46
|
+
expect(node.search('get', '/nothing')).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
it('/hello/foo, /hello/bar', () => {
|
|
49
|
+
expect(node.search('get', '/hello/foo').handler).toBe('get hello foo')
|
|
50
|
+
expect(node.search('post', '/hello/foo')).toBeNull()
|
|
51
|
+
expect(node.search('get', '/hello/bar')).toBeNull()
|
|
52
|
+
})
|
|
53
|
+
it('/hello/foo/bar', () => {
|
|
54
|
+
expect(node.search('get', '/hello/foo/bar')).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('Name path', () => {
|
|
59
|
+
const node = new Node()
|
|
60
|
+
it('get /entry/123', () => {
|
|
61
|
+
node.insert('get', '/entry/:id', 'get entry')
|
|
62
|
+
let res = node.search('get', '/entry/123')
|
|
63
|
+
expect(res).not.toBeNull()
|
|
64
|
+
expect(res.handler).toBe('get entry')
|
|
65
|
+
expect(res.params).not.toBeNull()
|
|
66
|
+
expect(res.params['id']).toBe('123')
|
|
67
|
+
expect(res.params['id']).not.toBe('1234')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('get /entry/456/comment', () => {
|
|
71
|
+
node.insert('get', '/entry/:id', 'get entry')
|
|
72
|
+
let res = node.search('get', '/entry/456/comment')
|
|
73
|
+
expect(res).toBeNull()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('get /entry/789/comment/123', () => {
|
|
77
|
+
node.insert('get', '/entry/:id/comment/:comment_id', 'get comment')
|
|
78
|
+
let res = node.search('get', '/entry/789/comment/123')
|
|
79
|
+
expect(res).not.toBeNull()
|
|
80
|
+
expect(res.handler).toBe('get comment')
|
|
81
|
+
expect(res.params['id']).toBe('789')
|
|
82
|
+
expect(res.params['comment_id']).toBe('123')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('get /map/:location/events', () => {
|
|
86
|
+
node.insert('get', '/map/:location/events', 'get events')
|
|
87
|
+
let res = node.search('get', '/map/yokohama/events')
|
|
88
|
+
expect(res).not.toBeNull()
|
|
89
|
+
expect(res.handler).toBe('get events')
|
|
90
|
+
expect(res.params['location']).toBe('yokohama')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('Wildcard', () => {
|
|
95
|
+
const node = new Node()
|
|
96
|
+
it('/wildcard-abc/xxxxxx/wildcard-efg', () => {
|
|
97
|
+
node.insert('get', '/wildcard-abc/*/wildcard-efg', 'wildcard')
|
|
98
|
+
let res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg')
|
|
99
|
+
expect(res).not.toBeNull()
|
|
100
|
+
expect(res.handler).toBe('wildcard')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('Regexp', () => {
|
|
105
|
+
const node = new Node()
|
|
106
|
+
node.insert('get', '/regex-abc/:id{[0-9]+}/comment/:comment_id{[a-z]+}', 'regexp')
|
|
107
|
+
it('/regexp-abc/123/comment/abc', () => {
|
|
108
|
+
res = node.search('get', '/regex-abc/123/comment/abc')
|
|
109
|
+
expect(res).not.toBeNull()
|
|
110
|
+
expect(res.handler).toBe('regexp')
|
|
111
|
+
expect(res.params['id']).toBe('123')
|
|
112
|
+
expect(res.params['comment_id']).toBe('abc')
|
|
113
|
+
})
|
|
114
|
+
it('/regexp-abc/abc', () => {
|
|
115
|
+
res = node.search('get', '/regex-abc/abc')
|
|
116
|
+
expect(res).toBeNull()
|
|
117
|
+
})
|
|
118
|
+
it('/regexp-abc/123/comment/123', () => {
|
|
119
|
+
res = node.search('get', '/regex-abc/123/comment/123')
|
|
120
|
+
expect(res).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('All', () => {
|
|
125
|
+
const node = new Node()
|
|
126
|
+
node.insert('ALL', '/all-methods', 'all methods') // ALL
|
|
127
|
+
it('/all-methods', () => {
|
|
128
|
+
res = node.search('get', '/all-methods')
|
|
129
|
+
expect(res).not.toBeNull()
|
|
130
|
+
expect(res.handler).toBe('all methods')
|
|
131
|
+
res = node.search('put', '/all-methods')
|
|
132
|
+
expect(res).not.toBeNull()
|
|
133
|
+
expect(res.handler).toBe('all methods')
|
|
134
|
+
})
|
|
135
|
+
})
|
package/src/router.test.js
CHANGED
|
@@ -1,66 +1,88 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
it('
|
|
7
|
-
router.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
expect(
|
|
13
|
-
})
|
|
1
|
+
const App = require('./hono')
|
|
2
|
+
|
|
3
|
+
describe('Basic Usage', () => {
|
|
4
|
+
const router = App()
|
|
5
|
+
|
|
6
|
+
it('get, post hello', async () => {
|
|
7
|
+
router.get('/hello', 'get hello')
|
|
8
|
+
router.post('/hello', 'post hello')
|
|
9
|
+
|
|
10
|
+
let res = await router.matchRoute('GET', '/hello')
|
|
11
|
+
expect(res).not.toBeNull()
|
|
12
|
+
expect(res.handler[0]).toBe('get hello')
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
expect(
|
|
14
|
+
res = await router.matchRoute('POST', '/hello')
|
|
15
|
+
expect(res).not.toBeNull()
|
|
16
|
+
expect(res.handler[0]).toBe('post hello')
|
|
17
|
+
|
|
18
|
+
res = await router.matchRoute('PUT', '/hello')
|
|
19
|
+
expect(res).toBeNull()
|
|
20
|
+
|
|
21
|
+
res = await router.matchRoute('GET', '/')
|
|
22
|
+
expect(res).toBeNull()
|
|
21
23
|
})
|
|
22
24
|
})
|
|
23
25
|
|
|
24
|
-
describe('
|
|
25
|
-
router
|
|
26
|
-
router.add('/entry/:id/:comment', 'entry-id-comment')
|
|
27
|
-
router.add('/year/:year{[0-9]{4}}/:month{[0-9]{2}}', 'date-regex')
|
|
26
|
+
describe('Complex', () => {
|
|
27
|
+
let router = App()
|
|
28
28
|
|
|
29
|
-
it('
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
expect(
|
|
29
|
+
it('Named Param', async () => {
|
|
30
|
+
router.get('/entry/:id', 'get entry')
|
|
31
|
+
res = await router.matchRoute('GET', '/entry/123')
|
|
32
|
+
expect(res).not.toBeNull()
|
|
33
|
+
expect(res.handler[0]).toBe('get entry')
|
|
34
|
+
expect(res.params['id']).toBe('123')
|
|
33
35
|
})
|
|
34
36
|
|
|
35
|
-
it('
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(
|
|
39
|
-
expect(
|
|
37
|
+
it('Wildcard', async () => {
|
|
38
|
+
router.get('/wild/*/card', 'get wildcard')
|
|
39
|
+
res = await router.matchRoute('GET', '/wild/xxx/card')
|
|
40
|
+
expect(res).not.toBeNull()
|
|
41
|
+
expect(res.handler[0]).toBe('get wildcard')
|
|
40
42
|
})
|
|
41
43
|
|
|
42
|
-
it('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
expect(
|
|
46
|
-
expect(
|
|
44
|
+
it('Regexp', async () => {
|
|
45
|
+
router.get('/post/:date{[0-9]+}/:title{[a-z]+}', 'get post')
|
|
46
|
+
res = await router.matchRoute('GET', '/post/20210101/hello')
|
|
47
|
+
expect(res).not.toBeNull()
|
|
48
|
+
expect(res.handler[0]).toBe('get post')
|
|
49
|
+
expect(res.params['date']).toBe('20210101')
|
|
50
|
+
expect(res.params['title']).toBe('hello')
|
|
51
|
+
res = await router.matchRoute('GET', '/post/onetwothree')
|
|
52
|
+
expect(res).toBeNull()
|
|
53
|
+
res = await router.matchRoute('GET', '/post/123/123')
|
|
54
|
+
expect(res).toBeNull()
|
|
47
55
|
})
|
|
56
|
+
})
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
describe('Chained Route', () => {
|
|
59
|
+
let router = App()
|
|
60
|
+
|
|
61
|
+
it('Return rooter object', async () => {
|
|
62
|
+
router = router.patch('/hello', 'patch hello')
|
|
63
|
+
expect(router).not.toBeNull()
|
|
64
|
+
router = router.delete('/hello', 'delete hello')
|
|
65
|
+
res = await router.matchRoute('DELETE', '/hello')
|
|
66
|
+
expect(res).not.toBeNull()
|
|
67
|
+
expect(res.handler[0]).toBe('delete hello')
|
|
54
68
|
})
|
|
55
|
-
})
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
router.
|
|
61
|
-
|
|
62
|
-
expect(
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
it('Chain with route method', async () => {
|
|
71
|
+
router.route('/api/book').get('get book').post('post book').put('put book')
|
|
72
|
+
|
|
73
|
+
res = await router.matchRoute('GET', '/api/book')
|
|
74
|
+
expect(res).not.toBeNull()
|
|
75
|
+
expect(res.handler[0]).toBe('get book')
|
|
76
|
+
|
|
77
|
+
res = await router.matchRoute('POST', '/api/book')
|
|
78
|
+
expect(res).not.toBeNull()
|
|
79
|
+
expect(res.handler[0]).toBe('post book')
|
|
80
|
+
|
|
81
|
+
res = await router.matchRoute('PUT', '/api/book')
|
|
82
|
+
expect(res).not.toBeNull()
|
|
83
|
+
expect(res.handler[0]).toBe('put book')
|
|
84
|
+
|
|
85
|
+
res = await router.matchRoute('DELETE', '/api/book')
|
|
86
|
+
expect(res).toBeNull()
|
|
65
87
|
})
|
|
66
88
|
})
|
package/src/util.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const splitPath = (path) => {
|
|
2
|
+
path = path.split(/\//) // faster than path.split('/')
|
|
3
|
+
if (path[0] === '') {
|
|
4
|
+
path.shift()
|
|
5
|
+
}
|
|
6
|
+
return path
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const getPattern = (label) => {
|
|
10
|
+
// :id{[0-9]+} => ([0-9]+)
|
|
11
|
+
// :id => (.+)
|
|
12
|
+
//const name = ''
|
|
13
|
+
const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/)
|
|
14
|
+
if (match) {
|
|
15
|
+
if (match[2]) {
|
|
16
|
+
return [match[1], '(' + match[2] + ')']
|
|
17
|
+
} else {
|
|
18
|
+
return [match[1], '(.+)']
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
splitPath,
|
|
25
|
+
getPattern,
|
|
26
|
+
}
|
package/src/util.test.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { splitPath, getPattern } = require('./util')
|
|
2
|
+
|
|
3
|
+
describe('Utility methods', () => {
|
|
4
|
+
it('splitPath', () => {
|
|
5
|
+
let ps = splitPath('/')
|
|
6
|
+
expect(ps[0]).toBe('')
|
|
7
|
+
ps = splitPath('/hello')
|
|
8
|
+
expect(ps[0]).toBe('hello')
|
|
9
|
+
ps = splitPath('*')
|
|
10
|
+
expect(ps[0]).toBe('*')
|
|
11
|
+
ps = splitPath('/wildcard-abc/*/wildcard-efg')
|
|
12
|
+
expect(ps[0]).toBe('wildcard-abc')
|
|
13
|
+
expect(ps[1]).toBe('*')
|
|
14
|
+
expect(ps[2]).toBe('wildcard-efg')
|
|
15
|
+
ps = splitPath('/map/:location/events')
|
|
16
|
+
expect(ps[0]).toBe('map')
|
|
17
|
+
expect(ps[1]).toBe(':location')
|
|
18
|
+
expect(ps[2]).toBe('events')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('getPattern', () => {
|
|
22
|
+
let res = getPattern(':id')
|
|
23
|
+
expect(res[0]).toBe('id')
|
|
24
|
+
expect(res[1]).toBe('(.+)')
|
|
25
|
+
res = getPattern(':id{[0-9]+}')
|
|
26
|
+
expect(res[0]).toBe('id')
|
|
27
|
+
expect(res[1]).toBe('([0-9]+)')
|
|
28
|
+
})
|
|
29
|
+
})
|
package/src/router.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Ref: https://github.com/bmf-san/goblin
|
|
2
|
-
|
|
3
|
-
class Router {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.node = new Node({ label: "/" })
|
|
6
|
-
}
|
|
7
|
-
add(path, stuff) {
|
|
8
|
-
this.node.insert(path, stuff);
|
|
9
|
-
}
|
|
10
|
-
match(path) {
|
|
11
|
-
return this.node.search(path);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class Node {
|
|
16
|
-
constructor({ label, stuff, children } = {}) {
|
|
17
|
-
this.label = label || "";
|
|
18
|
-
this.stuff = stuff || {};
|
|
19
|
-
this.children = children || [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
insert(path, stuff) {
|
|
23
|
-
let curNode = this
|
|
24
|
-
if (path == '/') {
|
|
25
|
-
curNode.label = path
|
|
26
|
-
curNode.stuff = stuff
|
|
27
|
-
}
|
|
28
|
-
const ps = this.splitPath(path)
|
|
29
|
-
for (const p of ps) {
|
|
30
|
-
let nextNode = curNode.children[p]
|
|
31
|
-
if (nextNode) {
|
|
32
|
-
curNode = nextNode
|
|
33
|
-
} else {
|
|
34
|
-
curNode.children[p] = new Node({ label: p, stuff: stuff, children: [] })
|
|
35
|
-
curNode = curNode.children[p]
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
splitPath(path) {
|
|
41
|
-
const ps = []
|
|
42
|
-
for (const p of path.split('/')) {
|
|
43
|
-
if (p) {
|
|
44
|
-
ps.push(p)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return ps
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
getPattern(label) {
|
|
51
|
-
// :id{[0-9]+} → [0-9]+$
|
|
52
|
-
// :id → (.+)
|
|
53
|
-
const match = label.match(/^\:.+?\{(.+)\}$/)
|
|
54
|
-
if (match) {
|
|
55
|
-
return '(' + match[1] + ')'
|
|
56
|
-
}
|
|
57
|
-
return '(.+)'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getParamName(label) {
|
|
61
|
-
const match = label.match(/^\:([^\{\}]+)/)
|
|
62
|
-
if (match) {
|
|
63
|
-
return match[1]
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
noRoute() {
|
|
68
|
-
return null
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
search(path) {
|
|
72
|
-
|
|
73
|
-
let curNode = this
|
|
74
|
-
const params = {}
|
|
75
|
-
|
|
76
|
-
for (const p of this.splitPath(path)) {
|
|
77
|
-
const nextNode = curNode.children[p]
|
|
78
|
-
if (nextNode) {
|
|
79
|
-
curNode = nextNode
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
if (Object.keys(curNode.children).length == 0) {
|
|
83
|
-
if (curNode.label != p) {
|
|
84
|
-
return this.noRoute()
|
|
85
|
-
}
|
|
86
|
-
break
|
|
87
|
-
}
|
|
88
|
-
let isParamMatch = false
|
|
89
|
-
for (const key in curNode.children) {
|
|
90
|
-
if (key == "*") { // Wildcard
|
|
91
|
-
curNode = curNode.children[key]
|
|
92
|
-
isParamMatch = true
|
|
93
|
-
break
|
|
94
|
-
}
|
|
95
|
-
if (key.match(/^:/)) {
|
|
96
|
-
const pattern = this.getPattern(key)
|
|
97
|
-
const match = p.match(new RegExp(pattern))
|
|
98
|
-
if (match) {
|
|
99
|
-
const k = this.getParamName(key)
|
|
100
|
-
params[k] = match[0]
|
|
101
|
-
curNode = curNode.children[key]
|
|
102
|
-
isParamMatch = true
|
|
103
|
-
break
|
|
104
|
-
}
|
|
105
|
-
return this.noRoute()
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (isParamMatch == false) {
|
|
109
|
-
return this.noRoute()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return [curNode.stuff, params]
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
module.exports = Router
|