hono 0.0.3 → 0.0.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.
- package/.github/workflows/ci.yml +24 -0
- package/README.md +111 -15
- package/package.json +3 -7
- package/src/compose.js +22 -0
- package/src/compose.test.js +42 -0
- package/src/hono.d.ts +42 -0
- package/src/hono.js +107 -68
- package/src/hono.test.js +93 -7
- package/src/methods.js +30 -0
- package/src/middleware/defaultFilter.js +19 -0
- package/src/middleware.js +3 -0
- package/src/node.js +68 -98
- package/src/node.test.js +43 -17
- package/src/router.test.js +43 -31
- package/src/util.js +26 -0
- package/src/util.test.js +29 -0
- package/example/basic/index.js +0 -7
- package/example/basic/package-lock.json +0 -13
- package/example/basic/package.json +0 -11
- package/example/basic/wrangler.toml +0 -8
- package/hono.mini.js +0 -1
- package/src/node.root.test.js +0 -17
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [ master ]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [ master ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ci:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [16.x]
|
|
15
|
+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v2
|
|
19
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
+
uses: actions/setup-node@v2
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ matrix.node-version }}
|
|
23
|
+
- run: yarn install --frozen-lockfile
|
|
24
|
+
- run: npm test
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Hono [炎] - Tiny web framework for Cloudflare Workers and others.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
-
const
|
|
6
|
+
const { Hono } = require('hono')
|
|
7
|
+
const app = new Hono()
|
|
7
8
|
|
|
8
9
|
app.get('/', () => new Response('Hono!!'))
|
|
9
10
|
|
|
@@ -15,11 +16,22 @@ app.fire()
|
|
|
15
16
|
- Fast - the router is implemented with Trie-Tree structure.
|
|
16
17
|
- Tiny - use only standard API.
|
|
17
18
|
- Portable - zero dependencies.
|
|
18
|
-
-
|
|
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
|
+
```
|
|
19
31
|
|
|
20
32
|
## Install
|
|
21
33
|
|
|
22
|
-
```
|
|
34
|
+
```
|
|
23
35
|
$ yarn add hono
|
|
24
36
|
```
|
|
25
37
|
|
|
@@ -29,21 +41,42 @@ or
|
|
|
29
41
|
$ npm install hono
|
|
30
42
|
```
|
|
31
43
|
|
|
44
|
+
## Methods
|
|
45
|
+
|
|
46
|
+
- app.**HTTP_METHOD**(path, callback)
|
|
47
|
+
- app.**all**(path, callback)
|
|
48
|
+
- app.**route**(path)
|
|
49
|
+
- app.**use**(path, middleware)
|
|
50
|
+
|
|
32
51
|
## Routing
|
|
33
52
|
|
|
34
53
|
### Basic
|
|
35
54
|
|
|
55
|
+
`app.HTTP_METHOD`
|
|
56
|
+
|
|
36
57
|
```js
|
|
37
|
-
|
|
38
|
-
app.
|
|
39
|
-
app.
|
|
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'))
|
|
40
73
|
```
|
|
41
74
|
|
|
42
75
|
### Named Parameter
|
|
43
76
|
|
|
44
77
|
```js
|
|
45
|
-
app.get('/user/:name', (
|
|
46
|
-
const name = req.params('name')
|
|
78
|
+
app.get('/user/:name', (c) => {
|
|
79
|
+
const name = c.req.params('name')
|
|
47
80
|
...
|
|
48
81
|
})
|
|
49
82
|
```
|
|
@@ -51,9 +84,9 @@ app.get('/user/:name', (req) => {
|
|
|
51
84
|
### Regexp
|
|
52
85
|
|
|
53
86
|
```js
|
|
54
|
-
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (
|
|
55
|
-
const date = req.params('date')
|
|
56
|
-
const title = req.params('title')
|
|
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')
|
|
57
90
|
...
|
|
58
91
|
```
|
|
59
92
|
|
|
@@ -62,15 +95,78 @@ app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (req) => {
|
|
|
62
95
|
```js
|
|
63
96
|
app
|
|
64
97
|
.route('/api/book')
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
.get(() => {...})
|
|
99
|
+
.post(() => {...})
|
|
100
|
+
.put(() => {...})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Middleware
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const logger = (c, next) => {
|
|
107
|
+
console.log(`[${c.req.method}] ${c.req.url}`)
|
|
108
|
+
next()
|
|
109
|
+
}
|
|
110
|
+
|
|
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
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
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
|
+
})
|
|
68
160
|
```
|
|
69
161
|
|
|
70
162
|
## Related projects
|
|
71
163
|
|
|
72
|
-
-
|
|
164
|
+
- koa <https://github.com/koajs/koa>
|
|
165
|
+
- express <https://github.com/expressjs/express>
|
|
166
|
+
- oak <https://github.com/oakserver/oak>
|
|
73
167
|
- itty-router <https://github.com/kwhitley/itty-router>
|
|
168
|
+
- Sunder <https://github.com/SunderJS/sunder>
|
|
169
|
+
- goblin <https://github.com/bmf-san/goblin>
|
|
74
170
|
|
|
75
171
|
## Author
|
|
76
172
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Minimal web framework for Cloudflare Workers",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Minimal web framework for Cloudflare Workers and Fastly Compute@Edge",
|
|
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",
|
|
@@ -16,9 +16,5 @@
|
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"jest": "^27.4.5",
|
|
18
18
|
"node-fetch": "^2.6.6"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"global": "^4.4.0",
|
|
22
|
-
"wrangler": "^0.0.0-beta.6"
|
|
23
19
|
}
|
|
24
20
|
}
|
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.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
declare class FetchEvent {}
|
|
2
|
+
declare class Request {}
|
|
3
|
+
declare class Response {}
|
|
4
|
+
declare class Context {}
|
|
5
|
+
|
|
6
|
+
declare class Node {}
|
|
7
|
+
|
|
8
|
+
declare class Router {
|
|
9
|
+
tempPath: string
|
|
10
|
+
node: Node
|
|
11
|
+
|
|
12
|
+
add(method: string, path: string, handlers: any[]): Node
|
|
13
|
+
match(method: string, path: string): Node
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Hono {
|
|
17
|
+
router: Router
|
|
18
|
+
middlewareRouters: Router[]
|
|
19
|
+
|
|
20
|
+
getRouter(): Router
|
|
21
|
+
addRoute(method: string, args: any[]): Hono
|
|
22
|
+
matchRoute(method: string, path: string): Node
|
|
23
|
+
createContext(req: Request, res: Response): Context
|
|
24
|
+
dispatch(req: Request, res: Response): Response
|
|
25
|
+
handleEvent(event: FetchEvent): Response
|
|
26
|
+
fire(): void
|
|
27
|
+
|
|
28
|
+
notFound(): Response
|
|
29
|
+
|
|
30
|
+
route(path: string): Hono
|
|
31
|
+
|
|
32
|
+
use(path: string, middleware: any): void
|
|
33
|
+
|
|
34
|
+
all(path: string, handler: any): Hono
|
|
35
|
+
get(path: string, handler: any): Hono
|
|
36
|
+
post(path: string, handler: any): Hono
|
|
37
|
+
put(path: string, handler: any): Hono
|
|
38
|
+
head(path: string, handler: any): Hono
|
|
39
|
+
delete(path: string, handler: any): Hono
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class Middleware {}
|
package/src/hono.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
1
3
|
const Node = require('./node')
|
|
4
|
+
const compose = require('./compose')
|
|
5
|
+
const methods = require('./methods')
|
|
6
|
+
const defaultFilter = require('./middleware/defaultFilter')
|
|
7
|
+
const Middleware = require('./middleware')
|
|
8
|
+
|
|
9
|
+
const METHOD_NAME_OF_ALL = 'ALL'
|
|
2
10
|
|
|
3
11
|
class Router {
|
|
4
12
|
constructor() {
|
|
@@ -6,98 +14,129 @@ class Router {
|
|
|
6
14
|
this.tempPath = '/'
|
|
7
15
|
}
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
|
|
17
|
+
add(method, path, ...handlers) {
|
|
18
|
+
this.node.insert(method, path, handlers)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
match(method, path) {
|
|
22
|
+
return this.node.search(method, path)
|
|
12
23
|
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getPathFromURL = (url) => {
|
|
27
|
+
// XXX
|
|
28
|
+
const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
|
|
29
|
+
return match[5]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Hono {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.router = new Router()
|
|
35
|
+
this.middlewareRouters = []
|
|
13
36
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
for (const method of methods) {
|
|
38
|
+
this[method] = (...args) => {
|
|
39
|
+
return this.addRoute(method, ...args)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const res = this.node.search(method, path)
|
|
22
|
-
return res
|
|
44
|
+
all(...args) {
|
|
45
|
+
this.addRoute('ALL', ...args)
|
|
23
46
|
}
|
|
24
47
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const response = this.filter(result)
|
|
28
|
-
return event.respondWith(response)
|
|
48
|
+
getRouter() {
|
|
49
|
+
return this.router
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
status: 200,
|
|
38
|
-
headers: {
|
|
39
|
-
'Content-Type': 'application/json',
|
|
40
|
-
},
|
|
41
|
-
})
|
|
52
|
+
addRoute(method, ...args) {
|
|
53
|
+
method = method.toUpperCase()
|
|
54
|
+
if (args.length === 1) {
|
|
55
|
+
this.router.add(method, this.router.tempPath, ...args)
|
|
56
|
+
} else {
|
|
57
|
+
this.router.add(method, ...args)
|
|
42
58
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
return this
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
route(path) {
|
|
63
|
+
this.router.tempPath = path
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
use(path, middleware) {
|
|
68
|
+
const router = new Router()
|
|
69
|
+
router.add(METHOD_NAME_OF_ALL, path, middleware)
|
|
70
|
+
this.middlewareRouters.push(router)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async matchRoute(method, path) {
|
|
74
|
+
return this.router.match(method, path)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// XXX
|
|
78
|
+
async createContext(req, res) {
|
|
79
|
+
return {
|
|
80
|
+
req: req,
|
|
81
|
+
res: res,
|
|
82
|
+
newResponse: (params) => {
|
|
83
|
+
return new Response(params)
|
|
84
|
+
},
|
|
50
85
|
}
|
|
51
|
-
return this.notFound()
|
|
52
86
|
}
|
|
53
87
|
|
|
54
|
-
dispatch(request) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
88
|
+
async dispatch(request, response) {
|
|
89
|
+
const [method, path] = [request.method, getPathFromURL(request.url)]
|
|
90
|
+
|
|
91
|
+
const result = await this.matchRoute(method, path)
|
|
92
|
+
if (!result) return this.notFound()
|
|
93
|
+
|
|
94
|
+
request.params = (key) => result.params[key]
|
|
95
|
+
|
|
96
|
+
let handler = result.handler[0] // XXX
|
|
97
|
+
|
|
98
|
+
const middleware = [defaultFilter] // add defaultFilter later
|
|
99
|
+
|
|
100
|
+
for (const mr of this.middlewareRouters) {
|
|
101
|
+
const mwResult = mr.match(METHOD_NAME_OF_ALL, path)
|
|
102
|
+
if (mwResult) {
|
|
103
|
+
middleware.push(...mwResult.handler)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
let wrappedHandler = async (context, next) => {
|
|
108
|
+
context.res = handler(context)
|
|
109
|
+
next()
|
|
62
110
|
}
|
|
63
111
|
|
|
64
|
-
|
|
65
|
-
|
|
112
|
+
middleware.push(wrappedHandler)
|
|
113
|
+
const composed = compose(middleware)
|
|
114
|
+
const c = await this.createContext(request, response)
|
|
115
|
+
|
|
116
|
+
composed(c)
|
|
117
|
+
|
|
118
|
+
return c.res
|
|
66
119
|
}
|
|
67
120
|
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
status: 404,
|
|
71
|
-
headers: {
|
|
72
|
-
'content-type': 'text/plain',
|
|
73
|
-
},
|
|
74
|
-
})
|
|
121
|
+
async handleEvent(event) {
|
|
122
|
+
return this.dispatch(event.request, {}) // XXX
|
|
75
123
|
}
|
|
76
124
|
|
|
77
125
|
fire() {
|
|
78
126
|
addEventListener('fetch', (event) => {
|
|
79
|
-
this.
|
|
127
|
+
event.respondWith(this.handleEvent(event))
|
|
80
128
|
})
|
|
81
129
|
}
|
|
82
|
-
}
|
|
83
130
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
(...args) => {
|
|
88
|
-
if (target.constructor.prototype.hasOwnProperty(prop)) {
|
|
89
|
-
return target[prop](...args)
|
|
90
|
-
} else {
|
|
91
|
-
if (args.length === 1) {
|
|
92
|
-
return target.addRoute(prop, target.tempPath, ...args)
|
|
93
|
-
}
|
|
94
|
-
return target.addRoute(prop, ...args)
|
|
95
|
-
}
|
|
96
|
-
},
|
|
131
|
+
notFound() {
|
|
132
|
+
return new Response('Not Found', { status: 404 })
|
|
133
|
+
}
|
|
97
134
|
}
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
// Default Export
|
|
137
|
+
module.exports = Hono
|
|
138
|
+
exports = module.exports
|
|
102
139
|
|
|
103
|
-
|
|
140
|
+
// Named Export
|
|
141
|
+
exports.Hono = Hono
|
|
142
|
+
exports.Middleware = Middleware
|
package/src/hono.test.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const Hono = require('./hono')
|
|
2
1
|
const fetch = require('node-fetch')
|
|
2
|
+
const { Hono } = require('./hono')
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
describe('GET Request', () => {
|
|
5
|
+
const app = new Hono()
|
|
5
6
|
|
|
6
|
-
describe('GET match', () => {
|
|
7
7
|
app.get('/hello', () => {
|
|
8
8
|
return new fetch.Response('hello', {
|
|
9
9
|
status: 200,
|
|
@@ -14,16 +14,102 @@ describe('GET match', () => {
|
|
|
14
14
|
status: 404,
|
|
15
15
|
})
|
|
16
16
|
}
|
|
17
|
-
it('GET /hello is ok', () => {
|
|
17
|
+
it('GET /hello is ok', async () => {
|
|
18
18
|
let req = new fetch.Request('https://example.com/hello')
|
|
19
|
-
let res = app.dispatch(req)
|
|
19
|
+
let res = await app.dispatch(req, new fetch.Response())
|
|
20
20
|
expect(res).not.toBeNull()
|
|
21
21
|
expect(res.status).toBe(200)
|
|
22
|
+
expect(await res.text()).toBe('hello')
|
|
22
23
|
})
|
|
23
|
-
it('GET / is not found', () => {
|
|
24
|
+
it('GET / is not found', async () => {
|
|
24
25
|
let req = new fetch.Request('https://example.com/')
|
|
25
|
-
let res = app.dispatch(req)
|
|
26
|
+
let res = await app.dispatch(req, new fetch.Response())
|
|
26
27
|
expect(res).not.toBeNull()
|
|
27
28
|
expect(res.status).toBe(404)
|
|
28
29
|
})
|
|
29
30
|
})
|
|
31
|
+
|
|
32
|
+
describe('params and query', () => {
|
|
33
|
+
const app = new Hono()
|
|
34
|
+
|
|
35
|
+
app.get('/entry/:id', async (c) => {
|
|
36
|
+
const id = await c.req.params('id')
|
|
37
|
+
return new fetch.Response(`id is ${id}`, {
|
|
38
|
+
status: 200,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
app.get('/search', async (c) => {
|
|
43
|
+
const name = await c.req.query('name')
|
|
44
|
+
return new fetch.Response(`name is ${name}`, {
|
|
45
|
+
status: 200,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('params of /entry/:id is found', async () => {
|
|
50
|
+
let req = new fetch.Request('https://example.com/entry/123')
|
|
51
|
+
let res = await app.dispatch(req)
|
|
52
|
+
expect(res.status).toBe(200)
|
|
53
|
+
expect(await res.text()).toBe('id is 123')
|
|
54
|
+
})
|
|
55
|
+
it('query of /search?name=sam is found', async () => {
|
|
56
|
+
let req = new fetch.Request('https://example.com/search?name=sam')
|
|
57
|
+
let res = await app.dispatch(req)
|
|
58
|
+
expect(res.status).toBe(200)
|
|
59
|
+
expect(await res.text()).toBe('name is sam')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('Middleware', () => {
|
|
64
|
+
const app = new Hono()
|
|
65
|
+
|
|
66
|
+
const logger = async (c, next) => {
|
|
67
|
+
console.log(`${c.req.method} : ${c.req.url}`)
|
|
68
|
+
next()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rootHeader = async (c, next) => {
|
|
72
|
+
next()
|
|
73
|
+
await c.res.headers.append('x-custom', 'root')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const customHeader = async (c, next) => {
|
|
77
|
+
next()
|
|
78
|
+
await c.res.headers.append('x-message', 'custom-header')
|
|
79
|
+
}
|
|
80
|
+
const customHeader2 = async (c, next) => {
|
|
81
|
+
next()
|
|
82
|
+
await c.res.headers.append('x-message-2', 'custom-header-2')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
app.use('*', logger)
|
|
86
|
+
app.use('*', rootHeader)
|
|
87
|
+
app.use('/hello', customHeader)
|
|
88
|
+
app.use('/hello/*', customHeader2)
|
|
89
|
+
app.get('/hello', () => {
|
|
90
|
+
return new fetch.Response('hello')
|
|
91
|
+
})
|
|
92
|
+
app.get('/hello/:message', (c) => {
|
|
93
|
+
const message = c.req.params('message')
|
|
94
|
+
return new fetch.Response(`${message}`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('logging and custom header', async () => {
|
|
98
|
+
let req = new fetch.Request('https://example.com/hello')
|
|
99
|
+
let res = await app.dispatch(req)
|
|
100
|
+
expect(res.status).toBe(200)
|
|
101
|
+
expect(await res.text()).toBe('hello')
|
|
102
|
+
expect(await res.headers.get('x-custom')).toBe('root')
|
|
103
|
+
expect(await res.headers.get('x-message')).toBe('custom-header')
|
|
104
|
+
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('logging and custom header with named params', async () => {
|
|
108
|
+
let req = new fetch.Request('https://example.com/hello/message')
|
|
109
|
+
let res = await app.dispatch(req)
|
|
110
|
+
expect(res.status).toBe(200)
|
|
111
|
+
expect(await res.text()).toBe('message')
|
|
112
|
+
expect(await res.headers.get('x-custom')).toBe('root')
|
|
113
|
+
expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
|
|
114
|
+
})
|
|
115
|
+
})
|
package/src/methods.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const methods = [
|
|
2
|
+
'get',
|
|
3
|
+
'post',
|
|
4
|
+
'put',
|
|
5
|
+
'head',
|
|
6
|
+
'delete',
|
|
7
|
+
'options',
|
|
8
|
+
'trace',
|
|
9
|
+
'copy',
|
|
10
|
+
'lock',
|
|
11
|
+
'mkcol',
|
|
12
|
+
'move',
|
|
13
|
+
'patch',
|
|
14
|
+
'purge',
|
|
15
|
+
'propfind',
|
|
16
|
+
'proppatch',
|
|
17
|
+
'unlock',
|
|
18
|
+
'report',
|
|
19
|
+
'mkactivity',
|
|
20
|
+
'checkout',
|
|
21
|
+
'merge',
|
|
22
|
+
'm-search',
|
|
23
|
+
'notify',
|
|
24
|
+
'subscribe',
|
|
25
|
+
'unsubscribe',
|
|
26
|
+
'search',
|
|
27
|
+
'connect',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
module.exports = methods
|
|
@@ -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
CHANGED
|
@@ -1,127 +1,97 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
constructor({ handler, params } = {}) {
|
|
5
|
-
this.handler = handler
|
|
6
|
-
this.params = params || {}
|
|
7
|
-
}
|
|
8
|
-
}
|
|
3
|
+
const { splitPath, getPattern } = require('./util')
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
constructor({ method, label, handler, children } = {}) {
|
|
12
|
-
this.label = label || ''
|
|
13
|
-
this.children = children || []
|
|
14
|
-
this.method = {}
|
|
15
|
-
if (method && handler) {
|
|
16
|
-
this.method[method] = handler
|
|
17
|
-
}
|
|
18
|
-
}
|
|
5
|
+
const METHOD_NAME_OF_ALL = 'ALL'
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
for (const p of ps) {
|
|
24
|
-
let nextNode = curNode.children[p]
|
|
25
|
-
if (nextNode) {
|
|
26
|
-
curNode = nextNode
|
|
27
|
-
} else {
|
|
28
|
-
curNode.children[p] = new Node({
|
|
29
|
-
label: p,
|
|
30
|
-
handler: handler,
|
|
31
|
-
})
|
|
32
|
-
curNode = curNode.children[p]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (!curNode.method[method]) {
|
|
36
|
-
curNode.method[method] = handler
|
|
37
|
-
}
|
|
38
|
-
}
|
|
7
|
+
const createResult = (handler, params) => {
|
|
8
|
+
return { handler: handler, params: params }
|
|
9
|
+
}
|
|
39
10
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (p) {
|
|
44
|
-
ps.push(p)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return ps
|
|
48
|
-
}
|
|
11
|
+
const noRoute = () => {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
49
14
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return '(' + match[1] + ')'
|
|
56
|
-
}
|
|
57
|
-
return '(.+)'
|
|
15
|
+
function Node(method, handler, children) {
|
|
16
|
+
this.children = children || {}
|
|
17
|
+
this.method = {}
|
|
18
|
+
if (method && handler) {
|
|
19
|
+
this.method[method] = handler
|
|
58
20
|
}
|
|
21
|
+
this.middlewares = []
|
|
22
|
+
}
|
|
59
23
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
32
|
}
|
|
33
|
+
curNode.children[p] = new Node(method, handler)
|
|
34
|
+
curNode = curNode.children[p]
|
|
65
35
|
}
|
|
36
|
+
curNode.method[method] = handler
|
|
37
|
+
return curNode
|
|
38
|
+
}
|
|
66
39
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const params = {}
|
|
40
|
+
Node.prototype.search = function (method, path) {
|
|
41
|
+
let curNode = this
|
|
70
42
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!root.method[method]) {
|
|
74
|
-
return this.noRoute()
|
|
75
|
-
}
|
|
76
|
-
}
|
|
43
|
+
const params = {}
|
|
44
|
+
const parts = splitPath(path)
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
53
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
64
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
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]
|
|
90
71
|
curNode = curNode.children[key]
|
|
91
72
|
isParamMatch = true
|
|
92
73
|
break
|
|
93
74
|
}
|
|
94
|
-
|
|
95
|
-
const pattern = this.getPattern(key)
|
|
96
|
-
const match = p.match(new RegExp(pattern))
|
|
97
|
-
if (match) {
|
|
98
|
-
const k = this.getParamName(key)
|
|
99
|
-
params[k] = match[0]
|
|
100
|
-
curNode = curNode.children[key]
|
|
101
|
-
isParamMatch = true
|
|
102
|
-
break
|
|
103
|
-
}
|
|
104
|
-
return this.noRoute()
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (isParamMatch == false) {
|
|
108
|
-
return this.noRoute()
|
|
75
|
+
return noRoute()
|
|
109
76
|
}
|
|
110
77
|
}
|
|
111
78
|
|
|
112
|
-
|
|
79
|
+
if (isWildcard) {
|
|
80
|
+
break
|
|
81
|
+
}
|
|
113
82
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
return res
|
|
117
|
-
} else {
|
|
118
|
-
return this.noRoute()
|
|
83
|
+
if (isParamMatch === false) {
|
|
84
|
+
return noRoute()
|
|
119
85
|
}
|
|
120
86
|
}
|
|
121
87
|
|
|
122
|
-
|
|
123
|
-
|
|
88
|
+
const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
|
|
89
|
+
|
|
90
|
+
if (!handler) {
|
|
91
|
+
return noRoute()
|
|
124
92
|
}
|
|
93
|
+
|
|
94
|
+
return createResult(handler, params)
|
|
125
95
|
}
|
|
126
96
|
|
|
127
97
|
module.exports = Node
|
package/src/node.test.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
const Node = require('./node')
|
|
2
|
-
const node = new Node()
|
|
3
2
|
|
|
4
|
-
describe('
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
expect(
|
|
10
|
-
expect(
|
|
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()
|
|
11
28
|
})
|
|
12
29
|
})
|
|
13
30
|
|
|
14
31
|
describe('Basic Usage', () => {
|
|
32
|
+
const node = new Node()
|
|
15
33
|
node.insert('get', '/hello', 'get hello')
|
|
16
34
|
node.insert('post', '/hello', 'post hello')
|
|
17
35
|
node.insert('get', '/hello/foo', 'get hello foo')
|
|
@@ -38,6 +56,7 @@ describe('Basic Usage', () => {
|
|
|
38
56
|
})
|
|
39
57
|
|
|
40
58
|
describe('Name path', () => {
|
|
59
|
+
const node = new Node()
|
|
41
60
|
it('get /entry/123', () => {
|
|
42
61
|
node.insert('get', '/entry/:id', 'get entry')
|
|
43
62
|
let res = node.search('get', '/entry/123')
|
|
@@ -50,35 +69,41 @@ describe('Name path', () => {
|
|
|
50
69
|
|
|
51
70
|
it('get /entry/456/comment', () => {
|
|
52
71
|
node.insert('get', '/entry/:id', 'get entry')
|
|
53
|
-
res = node.search('get', '/entry/456/comment')
|
|
72
|
+
let res = node.search('get', '/entry/456/comment')
|
|
54
73
|
expect(res).toBeNull()
|
|
55
74
|
})
|
|
56
75
|
|
|
57
76
|
it('get /entry/789/comment/123', () => {
|
|
58
77
|
node.insert('get', '/entry/:id/comment/:comment_id', 'get comment')
|
|
59
|
-
res = node.search('get', '/entry/789/comment/123')
|
|
78
|
+
let res = node.search('get', '/entry/789/comment/123')
|
|
60
79
|
expect(res).not.toBeNull()
|
|
61
80
|
expect(res.handler).toBe('get comment')
|
|
62
81
|
expect(res.params['id']).toBe('789')
|
|
63
82
|
expect(res.params['comment_id']).toBe('123')
|
|
64
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
|
+
})
|
|
65
92
|
})
|
|
66
93
|
|
|
67
94
|
describe('Wildcard', () => {
|
|
95
|
+
const node = new Node()
|
|
68
96
|
it('/wildcard-abc/xxxxxx/wildcard-efg', () => {
|
|
69
97
|
node.insert('get', '/wildcard-abc/*/wildcard-efg', 'wildcard')
|
|
70
|
-
res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg')
|
|
98
|
+
let res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg')
|
|
71
99
|
expect(res).not.toBeNull()
|
|
72
100
|
expect(res.handler).toBe('wildcard')
|
|
73
101
|
})
|
|
74
102
|
})
|
|
75
103
|
|
|
76
104
|
describe('Regexp', () => {
|
|
77
|
-
node
|
|
78
|
-
|
|
79
|
-
'/regex-abc/:id{[0-9]+}/comment/:comment_id{[a-z]+}',
|
|
80
|
-
'regexp'
|
|
81
|
-
)
|
|
105
|
+
const node = new Node()
|
|
106
|
+
node.insert('get', '/regex-abc/:id{[0-9]+}/comment/:comment_id{[a-z]+}', 'regexp')
|
|
82
107
|
it('/regexp-abc/123/comment/abc', () => {
|
|
83
108
|
res = node.search('get', '/regex-abc/123/comment/abc')
|
|
84
109
|
expect(res).not.toBeNull()
|
|
@@ -97,7 +122,8 @@ describe('Regexp', () => {
|
|
|
97
122
|
})
|
|
98
123
|
|
|
99
124
|
describe('All', () => {
|
|
100
|
-
node
|
|
125
|
+
const node = new Node()
|
|
126
|
+
node.insert('ALL', '/all-methods', 'all methods') // ALL
|
|
101
127
|
it('/all-methods', () => {
|
|
102
128
|
res = node.search('get', '/all-methods')
|
|
103
129
|
expect(res).not.toBeNull()
|
package/src/router.test.js
CHANGED
|
@@ -1,76 +1,88 @@
|
|
|
1
|
-
const
|
|
2
|
-
let router = Router()
|
|
1
|
+
const { Hono } = require('./hono')
|
|
3
2
|
|
|
4
3
|
describe('Basic Usage', () => {
|
|
5
|
-
|
|
4
|
+
const router = new Hono()
|
|
5
|
+
|
|
6
|
+
it('get, post hello', async () => {
|
|
6
7
|
router.get('/hello', 'get hello')
|
|
7
8
|
router.post('/hello', 'post hello')
|
|
8
9
|
|
|
9
|
-
let res = router.matchRoute('GET', '/hello')
|
|
10
|
+
let res = await router.matchRoute('GET', '/hello')
|
|
10
11
|
expect(res).not.toBeNull()
|
|
11
|
-
expect(res.handler).toBe('get hello')
|
|
12
|
+
expect(res.handler[0]).toBe('get hello')
|
|
12
13
|
|
|
13
|
-
res = router.matchRoute('POST', '/hello')
|
|
14
|
+
res = await router.matchRoute('POST', '/hello')
|
|
14
15
|
expect(res).not.toBeNull()
|
|
15
|
-
expect(res.handler).toBe('post hello')
|
|
16
|
+
expect(res.handler[0]).toBe('post hello')
|
|
16
17
|
|
|
17
|
-
res = router.matchRoute('PUT', '/hello')
|
|
18
|
+
res = await router.matchRoute('PUT', '/hello')
|
|
18
19
|
expect(res).toBeNull()
|
|
19
20
|
|
|
20
|
-
res = router.matchRoute('GET', '/')
|
|
21
|
+
res = await router.matchRoute('GET', '/')
|
|
21
22
|
expect(res).toBeNull()
|
|
22
23
|
})
|
|
23
24
|
})
|
|
24
25
|
|
|
25
26
|
describe('Complex', () => {
|
|
26
|
-
|
|
27
|
+
let router = new Hono()
|
|
28
|
+
|
|
29
|
+
it('Named Param', async () => {
|
|
27
30
|
router.get('/entry/:id', 'get entry')
|
|
28
|
-
res = router.matchRoute('GET', '/entry/123')
|
|
31
|
+
res = await router.matchRoute('GET', '/entry/123')
|
|
29
32
|
expect(res).not.toBeNull()
|
|
30
|
-
expect(res.handler).toBe('get entry')
|
|
33
|
+
expect(res.handler[0]).toBe('get entry')
|
|
31
34
|
expect(res.params['id']).toBe('123')
|
|
32
35
|
})
|
|
33
|
-
|
|
36
|
+
|
|
37
|
+
it('Wildcard', async () => {
|
|
34
38
|
router.get('/wild/*/card', 'get wildcard')
|
|
35
|
-
res = router.matchRoute('GET', '/wild/xxx/card')
|
|
39
|
+
res = await router.matchRoute('GET', '/wild/xxx/card')
|
|
36
40
|
expect(res).not.toBeNull()
|
|
37
|
-
expect(res.handler).toBe('get wildcard')
|
|
41
|
+
expect(res.handler[0]).toBe('get wildcard')
|
|
38
42
|
})
|
|
39
|
-
|
|
43
|
+
|
|
44
|
+
it('Regexp', async () => {
|
|
40
45
|
router.get('/post/:date{[0-9]+}/:title{[a-z]+}', 'get post')
|
|
41
|
-
res = router.matchRoute('GET', '/post/20210101/hello')
|
|
46
|
+
res = await router.matchRoute('GET', '/post/20210101/hello')
|
|
42
47
|
expect(res).not.toBeNull()
|
|
43
|
-
expect(res.handler).toBe('get post')
|
|
48
|
+
expect(res.handler[0]).toBe('get post')
|
|
44
49
|
expect(res.params['date']).toBe('20210101')
|
|
45
50
|
expect(res.params['title']).toBe('hello')
|
|
46
|
-
res = router.matchRoute('GET', '/post/onetwothree')
|
|
51
|
+
res = await router.matchRoute('GET', '/post/onetwothree')
|
|
47
52
|
expect(res).toBeNull()
|
|
48
|
-
res = router.matchRoute('GET', '/post/123/123')
|
|
53
|
+
res = await router.matchRoute('GET', '/post/123/123')
|
|
49
54
|
expect(res).toBeNull()
|
|
50
55
|
})
|
|
51
56
|
})
|
|
52
57
|
|
|
53
58
|
describe('Chained Route', () => {
|
|
54
|
-
|
|
59
|
+
let router = new Hono()
|
|
60
|
+
|
|
61
|
+
it('Return rooter object', async () => {
|
|
55
62
|
router = router.patch('/hello', 'patch hello')
|
|
56
63
|
expect(router).not.toBeNull()
|
|
57
64
|
router = router.delete('/hello', 'delete hello')
|
|
58
|
-
res = router.matchRoute('DELETE', '/hello')
|
|
65
|
+
res = await router.matchRoute('DELETE', '/hello')
|
|
59
66
|
expect(res).not.toBeNull()
|
|
60
|
-
expect(res.handler).toBe('delete hello')
|
|
67
|
+
expect(res.handler[0]).toBe('delete hello')
|
|
61
68
|
})
|
|
62
|
-
|
|
69
|
+
|
|
70
|
+
it('Chain with route method', async () => {
|
|
63
71
|
router.route('/api/book').get('get book').post('post book').put('put book')
|
|
64
|
-
|
|
72
|
+
|
|
73
|
+
res = await router.matchRoute('GET', '/api/book')
|
|
65
74
|
expect(res).not.toBeNull()
|
|
66
|
-
expect(res.handler).toBe('get book')
|
|
67
|
-
|
|
75
|
+
expect(res.handler[0]).toBe('get book')
|
|
76
|
+
|
|
77
|
+
res = await router.matchRoute('POST', '/api/book')
|
|
68
78
|
expect(res).not.toBeNull()
|
|
69
|
-
expect(res.handler).toBe('post book')
|
|
70
|
-
|
|
79
|
+
expect(res.handler[0]).toBe('post book')
|
|
80
|
+
|
|
81
|
+
res = await router.matchRoute('PUT', '/api/book')
|
|
71
82
|
expect(res).not.toBeNull()
|
|
72
|
-
expect(res.handler).toBe('put book')
|
|
73
|
-
|
|
83
|
+
expect(res.handler[0]).toBe('put book')
|
|
84
|
+
|
|
85
|
+
res = await router.matchRoute('DELETE', '/api/book')
|
|
74
86
|
expect(res).toBeNull()
|
|
75
87
|
})
|
|
76
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/example/basic/index.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "sandbox",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "",
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
|
|
10
|
-
"license": "ISC"
|
|
11
|
-
}
|
package/hono.mini.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const Router=require("./router");class Route{constructor(t,e){this.method=t;this.handler=e}}class App{constructor(){this.router=new Router}addRoute(t,e,n){this.router.add(e,new Route(t,n))}handle(t){const e=this.dispatch(t.request);return t.respondWith(e)}dispatch(t){const e=new URL(t.url);const n=e.pathname;const r=this.router.match(n);if(!r){return this.notFound()}const o=t.method.toLowerCase();const s=r[0];if(s.method==o){const i=s.handler;return i(t)}return this.notFound()}notFound(){return new Response("Not Found",{status:404,headers:{"content-type":"text/plain"}})}fire(){addEventListener("fetch",t=>{this.handle(t)})}}const proxyHandler={get:(e,n)=>(...t)=>{if(e.constructor.prototype.hasOwnProperty(n)){return e[n](t[0])}else{e.addRoute(n,t[0],t[1]);return}}};const app=new App;function Hono(){return new Proxy(app,proxyHandler)}module.exports=Hono;class Router{constructor(){this.node=new Node({label:"/"})}add(t,e){this.node.insert(t,e)}match(t){return this.node.search(t)}}class Node{constructor({label:t,stuff:e,children:n}={}){this.label=t||"";this.stuff=e||{};this.children=n||[]}insert(t,e){let n=this;if(t=="/"){n.label=t;n.stuff=e}const r=this.splitPath(t);for(const o of r){let t=n.children[o];if(t){n=t}else{n.children[o]=new Node({label:o,stuff:e,children:[]});n=n.children[o]}}}splitPath(t){const e=[];for(const n of t.split("/")){if(n){e.push(n)}}return e}getPattern(t){const e=t.match(/^\:.+?\{(.+)\}$/);if(e){return"("+e[1]+")"}return"(.+)"}getParamName(t){const e=t.match(/^\:([^\{\}]+)/);if(e){return e[1]}}noRoute(){return null}search(t){let e=this;const n={};for(const r of this.splitPath(t)){const o=e.children[r];if(o){e=o;continue}if(Object.keys(e.children).length==0){if(e.label!=r){return this.noRoute()}break}let t=false;for(const s in e.children){if(s=="*"){e=e.children[s];t=true;break}if(s.match(/^:/)){const i=this.getPattern(s);const c=r.match(new RegExp(i));if(c){const h=this.getParamName(s);n[h]=c[0];e=e.children[s];t=true;break}return this.noRoute()}}if(t==false){return this.noRoute()}}return[e.stuff,n]}}module.exports=Router;
|
package/src/node.root.test.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const Node = require('./node')
|
|
2
|
-
|
|
3
|
-
describe('Basic Usage', () => {
|
|
4
|
-
const node = new Node()
|
|
5
|
-
node.insert('get', '/', 'get root')
|
|
6
|
-
it('get /', () => {
|
|
7
|
-
expect(node.search('get', '/')).not.toBeNull()
|
|
8
|
-
})
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('Basic Usage', () => {
|
|
12
|
-
const node = new Node()
|
|
13
|
-
node.insert('get', '/hello', 'get hello')
|
|
14
|
-
it('get /', () => {
|
|
15
|
-
expect(node.search('get', '/')).toBeNull()
|
|
16
|
-
})
|
|
17
|
-
})
|