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.
@@ -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 app = Hono()
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
- - Optimized for Cloudflare Workers.
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
- ```sh
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
- app.get('/', () => 'GET /')
38
- app.post('/', () => 'POST /')
39
- app.get('/wild/*/card', () => 'GET /wild/*/card')
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', (req) => {
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]+}', (req) => {
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
- .get(() => 'GET /api/book')
66
- .post(() => 'POST /api/book')
67
- .put(() => 'PUT /api/book')
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
- - goblin <https://github.com/bmf-san/goblin>
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.3",
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 --verbose"
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
- route(path) {
10
- this.tempPath = path
11
- return WrappedRouter(this)
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
- addRoute(method, path, handler) {
15
- this.node.insert(method, path, handler)
16
- return WrappedRouter(this)
37
+ for (const method of methods) {
38
+ this[method] = (...args) => {
39
+ return this.addRoute(method, ...args)
40
+ }
41
+ }
17
42
  }
18
43
 
19
- matchRoute(method, path) {
20
- method = method.toLowerCase()
21
- const res = this.node.search(method, path)
22
- return res
44
+ all(...args) {
45
+ this.addRoute('ALL', ...args)
23
46
  }
24
47
 
25
- handle(event) {
26
- const result = this.dispatch(event.request)
27
- const response = this.filter(result)
28
- return event.respondWith(response)
48
+ getRouter() {
49
+ return this.router
29
50
  }
30
51
 
31
- filter(result) {
32
- if (result instanceof Response) {
33
- return result
34
- }
35
- if (typeof result === 'object') {
36
- return new Response(JSON.stringify(result), {
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
- if (typeof result === 'string') {
44
- return new Response(result, {
45
- status: 200,
46
- headers: {
47
- 'Content-Type': 'text/plain',
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 url = new URL(request.url)
56
- const path = url.pathname
57
- const method = request.method
58
- const res = this.matchRoute(method, path)
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
- if (!res) {
61
- return this.notFound()
107
+ let wrappedHandler = async (context, next) => {
108
+ context.res = handler(context)
109
+ next()
62
110
  }
63
111
 
64
- const handler = res.handler
65
- return handler(request)
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
- notFound() {
69
- return new Response('Not Found', {
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.handle(event)
127
+ event.respondWith(this.handleEvent(event))
80
128
  })
81
129
  }
82
- }
83
130
 
84
- const proxyHandler = {
85
- get:
86
- (target, prop) =>
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
- const WrappedRouter = (router = new Router()) => {
100
- return new Proxy(router, proxyHandler)
101
- }
136
+ // Default Export
137
+ module.exports = Hono
138
+ exports = module.exports
102
139
 
103
- module.exports = WrappedRouter
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
- const app = Hono()
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
@@ -0,0 +1,3 @@
1
+ class Middleware {}
2
+
3
+ module.exports = Middleware
package/src/node.js CHANGED
@@ -1,127 +1,97 @@
1
- const methodNameOfAll = 'all'
1
+ 'use strict'
2
2
 
3
- class Result {
4
- constructor({ handler, params } = {}) {
5
- this.handler = handler
6
- this.params = params || {}
7
- }
8
- }
3
+ const { splitPath, getPattern } = require('./util')
9
4
 
10
- class Node {
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
- insert(method, path, handler) {
21
- let curNode = this
22
- const ps = this.splitPath(path)
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
- splitPath(path) {
41
- let ps = ['/']
42
- for (const p of path.split('/')) {
43
- if (p) {
44
- ps.push(p)
45
- }
46
- }
47
- return ps
48
- }
11
+ const noRoute = () => {
12
+ return null
13
+ }
49
14
 
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 '(.+)'
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
- getParamName(label) {
61
- const match = label.match(/^\:([^\{\}]+)/)
62
- if (match) {
63
- return match[1]
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
- search(method, path) {
68
- let curNode = this
69
- const params = {}
40
+ Node.prototype.search = function (method, path) {
41
+ let curNode = this
70
42
 
71
- if (path === '/') {
72
- const root = this.children['/']
73
- if (!root.method[method]) {
74
- return this.noRoute()
75
- }
76
- }
43
+ const params = {}
44
+ const parts = splitPath(path)
77
45
 
78
- for (const p of this.splitPath(path)) {
79
- let nextNode = curNode.children[p]
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
- if (nextNode) {
82
- curNode = nextNode
83
- continue
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
- let isParamMatch = false
87
- for (const key in curNode.children) {
88
- if (key === '*') {
89
- // Wildcard
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
- if (key.match(/^:/)) {
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
- let handler = curNode.method[methodNameOfAll] || curNode.method[method]
79
+ if (isWildcard) {
80
+ break
81
+ }
113
82
 
114
- if (handler) {
115
- const res = new Result({ handler: handler, params: params })
116
- return res
117
- } else {
118
- return this.noRoute()
83
+ if (isParamMatch === false) {
84
+ return noRoute()
119
85
  }
120
86
  }
121
87
 
122
- noRoute() {
123
- return null
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('Util Methods', () => {
5
- it('node.splitPath', () => {
6
- let ps = node.splitPath('/')
7
- expect(ps[0]).toBe('/')
8
- ps = node.splitPath('/hello')
9
- expect(ps[0]).toBe('/')
10
- expect(ps[1]).toBe('hello')
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.insert(
78
- 'get',
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.insert('all', '/all-methods', 'all methods')
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()
@@ -1,76 +1,88 @@
1
- const Router = require('./hono')
2
- let router = Router()
1
+ const { Hono } = require('./hono')
3
2
 
4
3
  describe('Basic Usage', () => {
5
- it('get, post hello', () => {
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
- it('Named Param', () => {
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
- it('Wildcard', () => {
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
- it('Regexp', () => {
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
- it('Return rooter object', () => {
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
- it('Chain with route method', () => {
69
+
70
+ it('Chain with route method', async () => {
63
71
  router.route('/api/book').get('get book').post('post book').put('put book')
64
- res = router.matchRoute('GET', '/api/book')
72
+
73
+ res = await router.matchRoute('GET', '/api/book')
65
74
  expect(res).not.toBeNull()
66
- expect(res.handler).toBe('get book')
67
- res = router.matchRoute('POST', '/api/book')
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
- res = router.matchRoute('PUT', '/api/book')
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
- res = router.matchRoute('DELETE', '/api/book')
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
+ }
@@ -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
+ })
@@ -1,7 +0,0 @@
1
- const Hono = require('../../src/hono')
2
- const app = Hono()
3
-
4
- app.get('/', () => 'Hono!!')
5
- app.get('/hello', () => 'This is /hello')
6
-
7
- app.fire()
@@ -1,13 +0,0 @@
1
- {
2
- "name": "sandbox",
3
- "version": "1.0.0",
4
- "lockfileVersion": 2,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "sandbox",
9
- "version": "1.0.0",
10
- "license": "ISC"
11
- }
12
- }
13
- }
@@ -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
- }
@@ -1,8 +0,0 @@
1
- name = "basic"
2
- type = "webpack"
3
- route = ''
4
- zone_id = ''
5
- usage_model = ''
6
- compatibility_flags = []
7
- workers_dev = true
8
- compatibility_date = "2021-12-15"
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;
@@ -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
- })