mirror of
https://github.com/honojs/hono.git
synced 2024-11-21 18:18:57 +01:00
Migrate to TypeScript (#21)
* Migrate to TypeScript * ready for publish
This commit is contained in:
parent
ce6bd99118
commit
e6039f69f9
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
dist
|
||||
|
||||
# Cloudflare Workers
|
||||
worker
|
||||
|
||||
@ -92,7 +94,6 @@ out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
@ -1,3 +1,5 @@
|
||||
example
|
||||
benchmark
|
||||
.github
|
||||
.github
|
||||
tsconfig.json
|
||||
src
|
||||
|
77
README.md
77
README.md
@ -16,19 +16,21 @@ app.fire()
|
||||
## Feature
|
||||
|
||||
- Fast - the router is implemented with Trie-Tree structure.
|
||||
- Tiny - use only standard API.
|
||||
- Portable - zero dependencies.
|
||||
- Flexible - you can make your own middlewares.
|
||||
- Optimized - for Cloudflare Workers and Fastly Compute@Edge.
|
||||
- Easy - simple API, builtin middleware, and TypeScript support.
|
||||
- Optimized - for Cloudflare Workers or Fastly Compute@Edge.
|
||||
|
||||
## Benchmark
|
||||
|
||||
Hono is fastest!!
|
||||
|
||||
```
|
||||
hono x 813,001 ops/sec ±2.96% (75 runs sampled)
|
||||
itty-router x 160,415 ops/sec ±3.31% (85 runs sampled)
|
||||
sunder x 307,438 ops/sec ±4.77% (73 runs sampled)
|
||||
hono x 758,264 ops/sec ±5.41% (75 runs sampled)
|
||||
itty-router x 158,359 ops/sec ±3.21% (89 runs sampled)
|
||||
sunder x 297,581 ops/sec ±4.74% (83 runs sampled)
|
||||
Fastest is hono
|
||||
✨ Done in 37.03s.
|
||||
✨ Done in 42.84s.
|
||||
```
|
||||
|
||||
## Install
|
||||
@ -45,8 +47,8 @@ $ npm install hono
|
||||
|
||||
## Methods
|
||||
|
||||
- app.**HTTP_METHOD**(path, callback)
|
||||
- app.**all**(path, callback)
|
||||
- app.**HTTP_METHOD**(path, handler)
|
||||
- app.**all**(path, handler)
|
||||
- app.**route**(path)
|
||||
- app.**use**(path, middleware)
|
||||
|
||||
@ -120,25 +122,25 @@ const { Hono, Middleware } = require('hono')
|
||||
|
||||
...
|
||||
|
||||
app.use('*', Middleware.poweredBy)
|
||||
app.use('*', Middleware.poweredBy())
|
||||
app.use('*', Middleware.logger())
|
||||
|
||||
```
|
||||
|
||||
### Custom Middleware
|
||||
|
||||
```js
|
||||
const logger = async (c, next) => {
|
||||
// Custom logger
|
||||
app.use('*', async (c, next) => {
|
||||
console.log(`[${c.req.method}] ${c.req.url}`)
|
||||
await next()
|
||||
}
|
||||
})
|
||||
|
||||
const addHeader = async (c, next) => {
|
||||
// Add custom header
|
||||
app.use('/message/*', async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.add('x-message', 'This is middleware!')
|
||||
}
|
||||
|
||||
app.use('*', logger)
|
||||
app.use('/message/*', addHeader)
|
||||
})
|
||||
|
||||
app.get('/message/hello', () => 'Hello Middleware!')
|
||||
```
|
||||
@ -146,20 +148,18 @@ app.get('/message/hello', () => 'Hello Middleware!')
|
||||
### Custom 404 Response
|
||||
|
||||
```js
|
||||
const customNotFound = async (c, next) => {
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
if (c.res.status === 404) {
|
||||
c.res = new Response('Custom 404 Not Found', { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
app.use('*', customNotFound)
|
||||
})
|
||||
```
|
||||
|
||||
### Complex Pattern
|
||||
|
||||
```js
|
||||
// Log response time
|
||||
// Output response time
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
const responseTime = await c.res.headers.get('X-Response-Time')
|
||||
@ -180,38 +180,41 @@ app.use('*', async (c, next) => {
|
||||
### req
|
||||
|
||||
```js
|
||||
|
||||
// Get Request object
|
||||
app.get('/hello', (c) => {
|
||||
const userAgent = c.req.headers.get('User-Agent')
|
||||
...
|
||||
})
|
||||
|
||||
// Query params
|
||||
app.get('/search', (c) => {
|
||||
const query = c.req.query('q')
|
||||
...
|
||||
})
|
||||
|
||||
// Captured params
|
||||
app.get('/entry/:id', (c) => {
|
||||
const id = c.req.params('id')
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
### res
|
||||
|
||||
```js
|
||||
// Response object
|
||||
app.use('/', (c, next) => {
|
||||
next()
|
||||
c.res.headers.append('X-Debug', 'Debug message')
|
||||
})
|
||||
```
|
||||
|
||||
## Request
|
||||
|
||||
### query
|
||||
### text
|
||||
|
||||
```js
|
||||
app.get('/search', (c) => {
|
||||
const query = c.req.query('q')
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
### params
|
||||
|
||||
```js
|
||||
app.get('/entry/:id', (c) => {
|
||||
const id = c.req.params('id')
|
||||
...
|
||||
app.get('/say', (c) => {
|
||||
return c.text('Hello!')
|
||||
})
|
||||
```
|
||||
|
||||
@ -219,7 +222,7 @@ app.get('/entry/:id', (c) => {
|
||||
|
||||
Create your first Cloudflare Workers with Hono from scratch.
|
||||
|
||||
### Demo
|
||||
### How to setup
|
||||
|
||||
![Demo](https://user-images.githubusercontent.com/10682/147877447-ff5907cd-49be-4976-b3b4-5df2ac6dfda4.gif)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Benchmark from 'benchmark'
|
||||
import { makeEdgeEnv } from 'edge-mock'
|
||||
import { Hono } from '../../src/hono.js'
|
||||
import { Hono } from '../../dist/index'
|
||||
import itty from 'itty-router'
|
||||
const { Router: IttyRouter } = itty
|
||||
import { Router as SunderRouter, Sunder } from 'sunder'
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { Hono } = require('../../../src/hono')
|
||||
const { Hono } = require('../../../dist/index')
|
||||
|
||||
const hono = new Hono()
|
||||
hono.get('/user', () => new Response('User'))
|
||||
|
@ -1,17 +1,18 @@
|
||||
const { Hono, Middleware } = require('../../src/hono')
|
||||
const { Hono, Middleware } = require('../../dist/index')
|
||||
// or install from npm:
|
||||
// const { Hono, Middleware } = require('hono')
|
||||
const app = new Hono()
|
||||
|
||||
// Mount Builtin Middleware
|
||||
app.use('*', Middleware.poweredBy)
|
||||
app.use('*', Middleware.poweredBy())
|
||||
app.use('*', Middleware.logger())
|
||||
|
||||
// Custom Middleware
|
||||
// Add Custom Header
|
||||
const addHeader = async (c, next) => {
|
||||
app.use('/hello/*', async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.append('X-message', 'This is addHeader middleware!')
|
||||
}
|
||||
app.use('/hello/*', addHeader)
|
||||
})
|
||||
|
||||
// Log response time
|
||||
app.use('*', async (c, next) => {
|
||||
@ -29,7 +30,7 @@ app.use('*', async (c, next) => {
|
||||
})
|
||||
|
||||
// Routing
|
||||
app.get('/', () => 'Hono!!')
|
||||
app.get('/', () => new Response('Hono!!'))
|
||||
app.get('/hello', () => new Response('This is /hello'))
|
||||
app.get('/entry/:id', (c) => {
|
||||
const id = c.req.params('id')
|
||||
@ -42,6 +43,7 @@ app.get('/fetch-url', async () => {
|
||||
return new Response(`https://example.com/ is ${response.status}`)
|
||||
})
|
||||
|
||||
// Request headers
|
||||
app.get('/user-agent', (c) => {
|
||||
const userAgent = c.req.headers.get('User-Agent')
|
||||
return new Response(`Your UserAgent is ${userAgent}`)
|
||||
|
@ -1,7 +1,7 @@
|
||||
const { Hono } = require('hono')
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', (c, next) => {
|
||||
app.use('*', async (c, next) => {
|
||||
console.log(`[${c.req.method}] ${c.req.url}`)
|
||||
next()
|
||||
})
|
||||
|
6
jest.config.js
Normal file
6
jest.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
}
|
27
package.json
27
package.json
@ -1,10 +1,17 @@
|
||||
{
|
||||
"name": "hono",
|
||||
"version": "0.0.10",
|
||||
"description": "Minimal web framework for Cloudflare Workers and Fastly Compute@Edge",
|
||||
"main": "src/hono.js",
|
||||
"version": "0.0.11",
|
||||
"description": "Tiny web framework for Cloudflare Workers and others.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"build": "rimraf dist && tsc",
|
||||
"watch": "tsc -w",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
|
||||
"license": "MIT",
|
||||
@ -26,7 +33,13 @@
|
||||
"compute@edge"
|
||||
],
|
||||
"devDependencies": {
|
||||
"edge-mock": "^0.0.15",
|
||||
"jest": "^27.4.5"
|
||||
"@cloudflare/workers-types": "^3.3.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/service-worker-mock": "^2.0.1",
|
||||
"jest": "^27.4.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"service-worker-mock": "^2.0.5",
|
||||
"ts-jest": "^27.1.2",
|
||||
"typescript": "^4.5.4"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
// Based on the code in the MIT licensed `koa-compose` package.
|
||||
const compose = (middleware) => {
|
||||
return function (context, next) {
|
||||
export const compose = (middleware: any) => {
|
||||
return function (context: any, next?: Function) {
|
||||
let index = -1
|
||||
return dispatch(0)
|
||||
function dispatch(i) {
|
||||
function dispatch(i: number): any {
|
||||
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
|
||||
index = i
|
||||
let fn = middleware[i]
|
||||
@ -17,5 +17,3 @@ const compose = (middleware) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = compose
|
67
src/hono.d.ts
vendored
67
src/hono.d.ts
vendored
@ -1,67 +0,0 @@
|
||||
type Result = {
|
||||
handler: any
|
||||
params: {}
|
||||
}
|
||||
|
||||
declare class Node {
|
||||
method: string
|
||||
handler: any
|
||||
children: Node[]
|
||||
middlewares: any[]
|
||||
|
||||
insert(method: string, path: string, handler: any): Node
|
||||
search(method: string, path: string): Result
|
||||
}
|
||||
|
||||
declare class Context {
|
||||
req: Request
|
||||
res: Response
|
||||
newResponse(params: {}): Response
|
||||
}
|
||||
|
||||
type Handler = (c: Context, next: () => void) => Response | void
|
||||
|
||||
declare class Router {
|
||||
tempPath: string
|
||||
node: Node
|
||||
|
||||
add(method: string, path: string, handler: Handler): Node
|
||||
match(method: string, path: string): Node
|
||||
}
|
||||
|
||||
export class Hono {
|
||||
router: Router
|
||||
middlewareRouters: Router[]
|
||||
|
||||
use(path: string, middleware: Handler): void
|
||||
|
||||
route(path: string): Hono
|
||||
fire(): void
|
||||
|
||||
all(path: string, handler: Handler): Hono
|
||||
get(path: string, handler: Handler): Hono
|
||||
post(path: string, handler: Handler): Hono
|
||||
put(path: string, handler: Handler): Hono
|
||||
head(path: string, handler: Handler): Hono
|
||||
delete(path: string, handler: Handler): Hono
|
||||
|
||||
notFound(): Response
|
||||
|
||||
getRouter(): Router
|
||||
addRoute(method: string, args: any[]): Hono
|
||||
matchRoute(method: string, path: string): Promise<Node>
|
||||
createContext(req: Request, res: Response): Promise<Context>
|
||||
dispatch(req: Request, res: Response): Promise<Response>
|
||||
handleEvent(event: FetchEvent): Promise<Response>
|
||||
}
|
||||
|
||||
export class Middleware {
|
||||
static defaultFilter: Handler
|
||||
// Add builtin middlewares
|
||||
static poweredBy: Handler
|
||||
}
|
||||
|
||||
interface FetchEvent extends Event {
|
||||
request: Request
|
||||
respondWith(response: Promise<Response> | Response): Promise<Response>
|
||||
}
|
141
src/hono.js
141
src/hono.js
@ -1,141 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const Node = require('./node')
|
||||
const Middleware = require('./middleware')
|
||||
const compose = require('./compose')
|
||||
const methods = require('./methods')
|
||||
const { getPathFromURL } = require('./util')
|
||||
|
||||
const METHOD_NAME_OF_ALL = 'ALL'
|
||||
|
||||
class Router {
|
||||
constructor() {
|
||||
this.node = new Node()
|
||||
this.tempPath = '/'
|
||||
}
|
||||
|
||||
add(method, path, ...handlers) {
|
||||
this.node.insert(method, path, handlers)
|
||||
}
|
||||
|
||||
match(method, path) {
|
||||
return this.node.search(method, path)
|
||||
}
|
||||
}
|
||||
|
||||
class Hono {
|
||||
constructor() {
|
||||
this.router = new Router()
|
||||
this.middlewareRouters = []
|
||||
|
||||
for (const method of methods) {
|
||||
this[method] = (...args) => {
|
||||
return this.addRoute(method, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
all(...args) {
|
||||
this.addRoute('ALL', ...args)
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
return this.router
|
||||
}
|
||||
|
||||
addRoute(method, ...args) {
|
||||
method = method.toUpperCase()
|
||||
if (args.length === 1) {
|
||||
this.router.add(method, this.router.tempPath, ...args)
|
||||
} else {
|
||||
this.router.add(method, ...args)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
route(path) {
|
||||
this.router.tempPath = path
|
||||
return this
|
||||
}
|
||||
|
||||
use(path, middleware) {
|
||||
if (middleware.constructor.name !== 'AsyncFunction') {
|
||||
throw new TypeError('middleware must be a async function!')
|
||||
}
|
||||
|
||||
const router = new Router()
|
||||
router.add(METHOD_NAME_OF_ALL, path, middleware)
|
||||
this.middlewareRouters.push(router)
|
||||
}
|
||||
|
||||
async matchRoute(method, path) {
|
||||
return this.router.match(method, path)
|
||||
}
|
||||
|
||||
// XXX
|
||||
async createContext(req, res) {
|
||||
return {
|
||||
req: req,
|
||||
res: res,
|
||||
newResponse: (params) => {
|
||||
return new Response(params)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async dispatch(request, response) {
|
||||
const [method, path] = [request.method, getPathFromURL(request.url)]
|
||||
|
||||
const result = await this.matchRoute(method, path)
|
||||
|
||||
request.params = (key) => result.params[key]
|
||||
|
||||
let handler = result ? result.handler[0] : this.notFound // XXX
|
||||
|
||||
const middleware = []
|
||||
|
||||
for (const mr of this.middlewareRouters) {
|
||||
const mwResult = mr.match(METHOD_NAME_OF_ALL, path)
|
||||
if (mwResult) {
|
||||
middleware.push(...mwResult.handler)
|
||||
}
|
||||
}
|
||||
|
||||
let wrappedHandler = async (context, next) => {
|
||||
context.res = await handler(context)
|
||||
await next()
|
||||
}
|
||||
|
||||
middleware.push(Middleware.defaultFilter)
|
||||
middleware.push(wrappedHandler)
|
||||
|
||||
const composed = compose(middleware)
|
||||
const c = await this.createContext(request, response)
|
||||
|
||||
await composed(c)
|
||||
|
||||
return c.res
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
return this.dispatch(event.request, {}) // XXX
|
||||
}
|
||||
|
||||
fire() {
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(this.handleEvent(event))
|
||||
})
|
||||
}
|
||||
|
||||
notFound() {
|
||||
return new Response('Not Found', { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
// Default Export
|
||||
module.exports = Hono
|
||||
exports = module.exports
|
||||
|
||||
// Named Export
|
||||
exports.Hono = Hono
|
||||
exports.Middleware = Middleware
|
196
src/hono.ts
Normal file
196
src/hono.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { Node, Result } from './node'
|
||||
import { compose } from './compose'
|
||||
import { getPathFromURL } from './util'
|
||||
import { Middleware } from './middleware'
|
||||
|
||||
export { Middleware }
|
||||
|
||||
const METHOD_NAME_OF_ALL = 'ALL'
|
||||
|
||||
declare global {
|
||||
interface Request {
|
||||
params: (key: string) => any
|
||||
query: (key: string) => string | null
|
||||
}
|
||||
}
|
||||
|
||||
export class Context {
|
||||
req: Request
|
||||
res: Response
|
||||
|
||||
constructor(req: Request, res: Response) {
|
||||
this.req = req
|
||||
this.res = res
|
||||
}
|
||||
|
||||
newResponse(body?: BodyInit | null | undefined, init?: ResponseInit | undefined): Response {
|
||||
return new Response(body, init)
|
||||
}
|
||||
|
||||
text(body: string) {
|
||||
return this.newResponse(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = (c: Context, next?: Function) => Response | Promise<Response>
|
||||
type MiddlwareHandler = (c: Context, next: Function) => Promise<void>
|
||||
|
||||
export class Router<T> {
|
||||
node: Node<T>
|
||||
|
||||
constructor() {
|
||||
this.node = new Node()
|
||||
}
|
||||
|
||||
add(method: string, path: string, handler: T) {
|
||||
this.node.insert(method, path, handler)
|
||||
}
|
||||
|
||||
match(method: string, path: string): Result<T> | null {
|
||||
return this.node.search(method, path)
|
||||
}
|
||||
}
|
||||
|
||||
export class Hono {
|
||||
router: Router<Handler[]>
|
||||
middlewareRouters: Router<MiddlwareHandler>[]
|
||||
tempPath: string
|
||||
|
||||
constructor() {
|
||||
this.router = new Router()
|
||||
this.middlewareRouters = []
|
||||
this.tempPath = '/'
|
||||
}
|
||||
|
||||
/* HTTP METHODS */
|
||||
get(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('get', arg, ...args)
|
||||
}
|
||||
post(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('post', arg, ...args)
|
||||
}
|
||||
put(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('put', arg, ...args)
|
||||
}
|
||||
head(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('head', arg, ...args)
|
||||
}
|
||||
delete(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('delete', arg, ...args)
|
||||
}
|
||||
options(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('options', arg, ...args)
|
||||
}
|
||||
patch(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('patch', arg, ...args)
|
||||
}
|
||||
|
||||
/*
|
||||
trace
|
||||
copy
|
||||
lock
|
||||
purge
|
||||
unlock
|
||||
report
|
||||
checkout
|
||||
merge
|
||||
notify
|
||||
subscribe
|
||||
unsubscribe
|
||||
search
|
||||
connect
|
||||
*/
|
||||
|
||||
all(arg: string | Handler, ...args: Handler[]): Hono {
|
||||
return this.addRoute('all', arg, ...args)
|
||||
}
|
||||
|
||||
route(path: string): Hono {
|
||||
this.tempPath = path
|
||||
return this
|
||||
}
|
||||
|
||||
use(path: string, middleware: MiddlwareHandler): void {
|
||||
if (middleware.constructor.name !== 'AsyncFunction') {
|
||||
throw new TypeError('middleware must be a async function!')
|
||||
}
|
||||
|
||||
const router = new Router<MiddlwareHandler>()
|
||||
router.add(METHOD_NAME_OF_ALL, path, middleware)
|
||||
this.middlewareRouters.push(router)
|
||||
}
|
||||
|
||||
// addRoute('get', '/', handler)
|
||||
addRoute(method: string, arg: string | Handler, ...args: Handler[]): Hono {
|
||||
method = method.toUpperCase()
|
||||
if (typeof arg === 'string') {
|
||||
this.router.add(method, arg, args)
|
||||
} else {
|
||||
args.unshift(arg)
|
||||
this.router.add(method, this.tempPath, args)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
async matchRoute(method: string, path: string): Promise<Result<Handler[]>> {
|
||||
return this.router.match(method, path)
|
||||
}
|
||||
|
||||
async dispatch(request: Request, response?: Response) {
|
||||
const [method, path] = [request.method, getPathFromURL(request.url)]
|
||||
|
||||
const result = await this.matchRoute(method, path)
|
||||
|
||||
request.params = (key: string): string => {
|
||||
if (result) {
|
||||
return result.params[key]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
let handler = result ? result.handler[0] : this.notFound // XXX
|
||||
|
||||
const middleware = []
|
||||
|
||||
for (const mr of this.middlewareRouters) {
|
||||
const mwResult = mr.match(METHOD_NAME_OF_ALL, path)
|
||||
if (mwResult) {
|
||||
middleware.push(mwResult.handler)
|
||||
}
|
||||
}
|
||||
|
||||
let wrappedHandler = async (context: Context, next: Function) => {
|
||||
context.res = await handler(context)
|
||||
await next()
|
||||
}
|
||||
|
||||
middleware.push(Middleware.defaultFilter)
|
||||
middleware.push(wrappedHandler)
|
||||
|
||||
const composed = compose(middleware)
|
||||
const c = new Context(request, response)
|
||||
|
||||
await composed(c)
|
||||
|
||||
return c.res
|
||||
}
|
||||
|
||||
async handleEvent(event: FetchEvent): Promise<Response> {
|
||||
return this.dispatch(event.request)
|
||||
}
|
||||
|
||||
fire() {
|
||||
addEventListener('fetch', (event: FetchEvent): void => {
|
||||
event.respondWith(this.handleEvent(event))
|
||||
})
|
||||
}
|
||||
|
||||
notFound() {
|
||||
return new Response('Not Found', { status: 404 })
|
||||
}
|
||||
}
|
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Hono, Middleware, Context } from './hono'
|
@ -1,4 +1,4 @@
|
||||
const methods = [
|
||||
export const methods = [
|
||||
'get',
|
||||
'post',
|
||||
'put',
|
||||
@ -26,5 +26,3 @@ const methods = [
|
||||
'search',
|
||||
'connect',
|
||||
]
|
||||
|
||||
module.exports = methods
|
@ -1,11 +0,0 @@
|
||||
const defaultFilter = require('./middleware/defaultFilter')
|
||||
const poweredBy = require('./middleware/poweredBy/poweredBy')
|
||||
const logger = require('./middleware/logger/logger')
|
||||
|
||||
function Middleware() {}
|
||||
|
||||
Middleware.defaultFilter = defaultFilter
|
||||
Middleware.poweredBy = poweredBy
|
||||
Middleware.logger = logger
|
||||
|
||||
module.exports = Middleware
|
9
src/middleware.ts
Normal file
9
src/middleware.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defaultFilter } from './middleware/defaultFilter'
|
||||
import { poweredBy } from './middleware/poweredBy/poweredBy'
|
||||
import { logger } from './middleware/logger/logger'
|
||||
|
||||
export class Middleware {
|
||||
static defaultFilter = defaultFilter
|
||||
static poweredBy = poweredBy
|
||||
static logger = logger
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
const defaultFilter = async (c, next) => {
|
||||
c.req.query = (key) => {
|
||||
const url = new URL(c.req.url)
|
||||
return url.searchParams.get(key)
|
||||
}
|
||||
|
||||
await next()
|
||||
|
||||
if (typeof c.res === 'string') {
|
||||
c.res = new Response(c.res, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Conten-Type': 'text/plain',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = defaultFilter
|
10
src/middleware/defaultFilter.ts
Normal file
10
src/middleware/defaultFilter.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Context } from '../hono'
|
||||
|
||||
export const defaultFilter = async (c: Context, next: Function) => {
|
||||
c.req.query = (key: string) => {
|
||||
const url = new URL(c.req.url)
|
||||
return url.searchParams.get(key)
|
||||
}
|
||||
|
||||
await next()
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
const { makeEdgeEnv } = require('edge-mock')
|
||||
const { Hono, Middleware } = require('../../hono')
|
||||
import makeServiceWorkerEnv from 'service-worker-mock'
|
||||
import { Hono, Middleware } from '../../hono'
|
||||
|
||||
makeEdgeEnv()
|
||||
declare var global: any
|
||||
Object.assign(global, makeServiceWorkerEnv())
|
||||
|
||||
describe('Logger by Middleware', () => {
|
||||
const app = new Hono()
|
||||
|
||||
let log = ''
|
||||
const logFn = (str) => {
|
||||
const logFn = (str: string) => {
|
||||
log = str
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
const { getPathFromURL } = require('../../util')
|
||||
import { getPathFromURL } from '../../util'
|
||||
import { Context } from '../../hono'
|
||||
|
||||
const humanize = (n, opts) => {
|
||||
const humanize = (n: string[], opts?: any) => {
|
||||
const options = opts || {}
|
||||
const d = options.delimiter || ','
|
||||
const s = options.separator || '.'
|
||||
@ -9,9 +10,9 @@ const humanize = (n, opts) => {
|
||||
return n.join(s)
|
||||
}
|
||||
|
||||
const time = (start) => {
|
||||
const time = (start: number) => {
|
||||
const delta = Date.now() - start
|
||||
return humanize(delta < 10000 ? delta + 'ms' : Math.round(delta / 1000) + 's')
|
||||
return humanize([delta < 10000 ? delta + 'ms' : Math.round(delta / 1000) + 's'])
|
||||
}
|
||||
|
||||
const LogPrefix = {
|
||||
@ -20,8 +21,8 @@ const LogPrefix = {
|
||||
Error: 'xxx',
|
||||
}
|
||||
|
||||
const colorStatus = (status) => {
|
||||
const out = {
|
||||
const colorStatus = (status: number = 0) => {
|
||||
const out: { [key: number]: string } = {
|
||||
7: `\x1b[35m${status}\x1b[0m`,
|
||||
5: `\x1b[31m${status}\x1b[0m`,
|
||||
4: `\x1b[33m${status}\x1b[0m`,
|
||||
@ -32,7 +33,7 @@ const colorStatus = (status) => {
|
||||
}
|
||||
return out[(status / 100) | 0]
|
||||
}
|
||||
function log(fn, prefix, method, path, status, elasped) {
|
||||
function log(fn: Function, prefix: string, method: string, path: string, status?: number, elasped?: string) {
|
||||
const out =
|
||||
prefix === LogPrefix.Incoming
|
||||
? ` ${prefix} ${method} ${path}`
|
||||
@ -40,8 +41,8 @@ function log(fn, prefix, method, path, status, elasped) {
|
||||
fn(out)
|
||||
}
|
||||
|
||||
const logger = (fn = console.log) => {
|
||||
return async (c, next) => {
|
||||
export const logger = (fn = console.log) => {
|
||||
return async (c: Context, next: Function) => {
|
||||
const { method } = c.req
|
||||
const path = getPathFromURL(c.req.url)
|
||||
|
||||
@ -59,5 +60,3 @@ const logger = (fn = console.log) => {
|
||||
log(fn, LogPrefix.Outgoing, method, path, c.res.status, time(start))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = logger
|
@ -1,6 +0,0 @@
|
||||
const poweredBy = async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.append('X-Powered-By', 'Hono')
|
||||
}
|
||||
|
||||
module.exports = poweredBy
|
@ -1,12 +1,13 @@
|
||||
const { makeEdgeEnv } = require('edge-mock')
|
||||
const { Hono, Middleware } = require('../../hono')
|
||||
import makeServiceWorkerEnv from 'service-worker-mock'
|
||||
import { Hono, Middleware } from '../../hono'
|
||||
|
||||
makeEdgeEnv()
|
||||
declare var global: any
|
||||
Object.assign(global, makeServiceWorkerEnv())
|
||||
|
||||
describe('Powered by Middleware', () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', Middleware.poweredBy)
|
||||
app.use('*', Middleware.poweredBy())
|
||||
app.get('/', () => new Response('root'))
|
||||
|
||||
it('Response headers include X-Powered-By', async () => {
|
9
src/middleware/poweredBy/poweredBy.ts
Normal file
9
src/middleware/poweredBy/poweredBy.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Context } from '../../hono'
|
||||
|
||||
export const poweredBy = () => {
|
||||
return async (c: Context, next: Function) => {
|
||||
await next()
|
||||
// await c.res.headers.append('X-Powered-By', 'Hono')
|
||||
c.res.headers.append('X-Powered-By', 'Hono')
|
||||
}
|
||||
}
|
97
src/node.js
97
src/node.js
@ -1,97 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { splitPath, getPattern } = require('./util')
|
||||
|
||||
const METHOD_NAME_OF_ALL = 'ALL'
|
||||
|
||||
const createResult = (handler, params) => {
|
||||
return { handler: handler, params: params }
|
||||
}
|
||||
|
||||
const noRoute = () => {
|
||||
return null
|
||||
}
|
||||
|
||||
function Node(method, handler, children) {
|
||||
this.children = children || {}
|
||||
this.method = {}
|
||||
if (method && handler) {
|
||||
this.method[method] = handler
|
||||
}
|
||||
this.middlewares = []
|
||||
}
|
||||
|
||||
Node.prototype.insert = function (method, path, handler) {
|
||||
let curNode = this
|
||||
const parts = splitPath(path)
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i]
|
||||
if (Object.keys(curNode.children).includes(p)) {
|
||||
curNode = curNode.children[p]
|
||||
continue
|
||||
}
|
||||
curNode.children[p] = new Node(method, handler)
|
||||
curNode = curNode.children[p]
|
||||
}
|
||||
curNode.method[method] = handler
|
||||
return curNode
|
||||
}
|
||||
|
||||
Node.prototype.search = function (method, path) {
|
||||
let curNode = this
|
||||
|
||||
const params = {}
|
||||
const parts = splitPath(path)
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const p = parts[i]
|
||||
const nextNode = curNode.children[p]
|
||||
if (nextNode) {
|
||||
curNode = nextNode
|
||||
continue
|
||||
}
|
||||
|
||||
let isWildcard = false
|
||||
let isParamMatch = false
|
||||
const keys = Object.keys(curNode.children)
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
const key = keys[j]
|
||||
// Wildcard
|
||||
if (key === '*') {
|
||||
curNode = curNode.children['*']
|
||||
isWildcard = true
|
||||
break
|
||||
}
|
||||
const pattern = getPattern(key)
|
||||
if (pattern) {
|
||||
const match = p.match(new RegExp(pattern[1]))
|
||||
if (match) {
|
||||
const k = pattern[0]
|
||||
params[k] = match[1]
|
||||
curNode = curNode.children[key]
|
||||
isParamMatch = true
|
||||
break
|
||||
}
|
||||
return noRoute()
|
||||
}
|
||||
}
|
||||
|
||||
if (isWildcard) {
|
||||
break
|
||||
}
|
||||
|
||||
if (isParamMatch === false) {
|
||||
return noRoute()
|
||||
}
|
||||
}
|
||||
|
||||
const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
|
||||
|
||||
if (!handler) {
|
||||
return noRoute()
|
||||
}
|
||||
|
||||
return createResult(handler, params)
|
||||
}
|
||||
|
||||
module.exports = Node
|
123
src/node.ts
Normal file
123
src/node.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { splitPath, getPattern } from './util'
|
||||
|
||||
const METHOD_NAME_OF_ALL = 'ALL'
|
||||
|
||||
export class Result<T> {
|
||||
handler: T
|
||||
params: { [key: string]: string }
|
||||
constructor(handler: T, params: { [key: string]: string }) {
|
||||
this.handler = handler
|
||||
this.params = params
|
||||
}
|
||||
}
|
||||
|
||||
const noRoute = (): null => {
|
||||
return null
|
||||
}
|
||||
|
||||
export class Node<T> {
|
||||
method: { [key: string]: T }
|
||||
handler: T
|
||||
children: { [key: string]: Node<T> }
|
||||
middlewares: []
|
||||
|
||||
constructor(method?: string, handler?: any, children?: { [key: string]: Node<T> }) {
|
||||
this.children = children || {}
|
||||
this.method = {}
|
||||
if (method && handler) {
|
||||
this.method[method] = handler
|
||||
}
|
||||
this.middlewares = []
|
||||
}
|
||||
|
||||
insert(method: string, path: string, handler: T): Node<T> {
|
||||
let curNode: Node<T> = this
|
||||
const parts = splitPath(path)
|
||||
for (let i = 0, len = parts.length; i < len; i++) {
|
||||
const p: string = parts[i]
|
||||
if (Object.keys(curNode.children).includes(p)) {
|
||||
curNode = curNode.children[p]
|
||||
continue
|
||||
}
|
||||
curNode.children[p] = new Node()
|
||||
curNode = curNode.children[p]
|
||||
}
|
||||
curNode.method[method] = handler
|
||||
return curNode
|
||||
}
|
||||
|
||||
search(method: string, path: string): Result<T> {
|
||||
let curNode: Node<T> = this
|
||||
|
||||
const params: { [key: string]: string } = {}
|
||||
let parts = splitPath(path)
|
||||
|
||||
for (let i = 0, len = parts.length; i < len; i++) {
|
||||
const p: string = parts[i]
|
||||
|
||||
// '*' => match any path
|
||||
if (curNode.children['*']) {
|
||||
const astNode = curNode.children['*']
|
||||
if (Object.keys(astNode.children).length === 0) {
|
||||
curNode = astNode
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const nextNode = curNode.children[p]
|
||||
|
||||
if (nextNode) {
|
||||
curNode = nextNode
|
||||
// '/hello/*' => match '/hello'
|
||||
if (!(i == len - 1 && nextNode.children['*'])) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let isWildcard = false
|
||||
let isParamMatch = false
|
||||
const keys = Object.keys(curNode.children)
|
||||
|
||||
for (let j = 0, len = keys.length; j < len; j++) {
|
||||
const key: string = keys[j]
|
||||
|
||||
// Wildcard
|
||||
// '/hello/*/foo' => match /hello/bar/foo
|
||||
if (key === '*') {
|
||||
curNode = curNode.children['*']
|
||||
isWildcard = true
|
||||
break
|
||||
}
|
||||
const pattern = getPattern(key)
|
||||
// Named match
|
||||
if (pattern) {
|
||||
const match = p.match(new RegExp(pattern[1]))
|
||||
if (match) {
|
||||
const k: string = pattern[0]
|
||||
params[k] = match[1]
|
||||
curNode = curNode.children[key]
|
||||
isParamMatch = true
|
||||
break
|
||||
}
|
||||
return noRoute()
|
||||
}
|
||||
}
|
||||
|
||||
if (isWildcard && i === len - 1) {
|
||||
break
|
||||
}
|
||||
|
||||
if (isWildcard === false && isParamMatch === false) {
|
||||
return noRoute()
|
||||
}
|
||||
}
|
||||
|
||||
const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
|
||||
|
||||
if (!handler) {
|
||||
return noRoute()
|
||||
}
|
||||
|
||||
return new Result<T>(handler, params)
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
const splitPath = (path) => {
|
||||
path = path.split(/\//) // faster than path.split('/')
|
||||
if (path[0] === '') {
|
||||
path.shift()
|
||||
export const splitPath = (path: string): string[] => {
|
||||
const paths = path.split(/\//) // faster than path.split('/')
|
||||
if (paths[0] === '') {
|
||||
paths.shift()
|
||||
}
|
||||
return path
|
||||
return paths
|
||||
}
|
||||
|
||||
const getPattern = (label) => {
|
||||
export const getPattern = (label: string): string[] | null => {
|
||||
// :id{[0-9]+} => ([0-9]+)
|
||||
// :id => (.+)
|
||||
//const name = ''
|
||||
@ -18,16 +18,14 @@ const getPattern = (label) => {
|
||||
return [match[1], '(.+)']
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getPathFromURL = (url) => {
|
||||
export const getPathFromURL = (url: string) => {
|
||||
// XXX
|
||||
const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
|
||||
return match[5]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
splitPath,
|
||||
getPattern,
|
||||
getPathFromURL,
|
||||
if (match) {
|
||||
return match[5]
|
||||
}
|
||||
return ''
|
||||
}
|
@ -1,21 +1,26 @@
|
||||
const compose = require('../src/compose')
|
||||
import { compose } from '../src/compose'
|
||||
|
||||
class C {
|
||||
req: { [key: string]: any }
|
||||
res: { [key: string]: any }
|
||||
}
|
||||
|
||||
describe('compose middleware', () => {
|
||||
const middleware = []
|
||||
const middleware: Function[] = []
|
||||
|
||||
const a = async (c, next) => {
|
||||
const a = async (c: C, next: Function) => {
|
||||
c.req['log'] = 'log'
|
||||
await next()
|
||||
}
|
||||
middleware.push(a)
|
||||
|
||||
const b = async (c, next) => {
|
||||
const b = async (c: C, next: Function) => {
|
||||
await next()
|
||||
c.res['header'] = `${c.res.header}-custom-header`
|
||||
c.res['headers'] = `${c.res.headers}-custom-header`
|
||||
}
|
||||
middleware.push(b)
|
||||
|
||||
const handler = async (c, next) => {
|
||||
const handler = async (c: C, next: Function) => {
|
||||
c.req['log'] = `${c.req.log} message`
|
||||
await next()
|
||||
c.res = { message: 'new response' }
|
||||
@ -26,14 +31,14 @@ describe('compose middleware', () => {
|
||||
const response = {}
|
||||
|
||||
it('Request', async () => {
|
||||
const c = { req: request, res: response }
|
||||
const c: C = { req: request, res: response }
|
||||
const composed = compose(middleware)
|
||||
await composed(c)
|
||||
expect(c.req['log']).not.toBeNull()
|
||||
expect(c.req['log']).toBe('log message')
|
||||
})
|
||||
it('Response', async () => {
|
||||
const c = { req: request, res: response }
|
||||
const c: C = { req: request, res: response }
|
||||
const composed = compose(middleware)
|
||||
await composed(c)
|
||||
expect(c.res['header']).not.toBeNull()
|
@ -1,11 +1,11 @@
|
||||
const { makeEdgeEnv } = require('edge-mock')
|
||||
const { Hono } = require('../src/hono')
|
||||
import makeServiceWorkerEnv from 'service-worker-mock'
|
||||
import { Hono, Context } from '../src/hono'
|
||||
|
||||
makeEdgeEnv()
|
||||
declare var global: any
|
||||
Object.assign(global, makeServiceWorkerEnv())
|
||||
|
||||
describe('GET Request', () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/hello', () => {
|
||||
return new Response('hello', {
|
||||
status: 200,
|
||||
@ -31,6 +31,42 @@ describe('GET Request', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Routing', () => {
|
||||
const app = new Hono()
|
||||
|
||||
it('Return it self', async () => {
|
||||
const appRes = app.get('/', () => new Response('get /'))
|
||||
expect(appRes).not.toBeUndefined()
|
||||
appRes.delete('/', () => new Response('delete /'))
|
||||
let req = new Request('/', { method: 'DELETE' })
|
||||
const res = await appRes.dispatch(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('delete /')
|
||||
})
|
||||
|
||||
it('Chained route', async () => {
|
||||
app
|
||||
.route('/route')
|
||||
.get(() => new Response('get /route'))
|
||||
.post(() => new Response('post /route'))
|
||||
.put(() => new Response('put /route'))
|
||||
let req = new Request('/route', { method: 'GET' })
|
||||
let res = await app.dispatch(req)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('get /route')
|
||||
|
||||
req = new Request('/route', { method: 'POST' })
|
||||
res = await app.dispatch(req)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('post /route')
|
||||
|
||||
req = new Request('/route', { method: 'DELETE' })
|
||||
res = await app.dispatch(req)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
})
|
||||
|
||||
describe('params and query', () => {
|
||||
const app = new Hono()
|
||||
|
||||
@ -65,29 +101,27 @@ describe('params and query', () => {
|
||||
describe('Middleware', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const logger = async (c, next) => {
|
||||
// Custom Logger
|
||||
app.use('*', async (c, next) => {
|
||||
console.log(`${c.req.method} : ${c.req.url}`)
|
||||
await next()
|
||||
}
|
||||
})
|
||||
|
||||
const rootHeader = async (c, next) => {
|
||||
// Apeend Custom Header
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.append('x-custom', 'root')
|
||||
}
|
||||
})
|
||||
|
||||
const customHeader = async (c, next) => {
|
||||
app.use('/hello', async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.append('x-message', 'custom-header')
|
||||
}
|
||||
const customHeader2 = async (c, next) => {
|
||||
await next()
|
||||
c.res.headers.append('x-message-2', 'custom-header-2')
|
||||
}
|
||||
})
|
||||
|
||||
app.use('*', logger)
|
||||
app.use('*', rootHeader)
|
||||
app.use('/hello', customHeader)
|
||||
app.use('/hello/*', customHeader2)
|
||||
app.use('/hello/*', async (c, next) => {
|
||||
await next()
|
||||
await c.res.headers.append('x-message-2', 'custom-header-2')
|
||||
})
|
||||
|
||||
app.get('/hello', () => {
|
||||
return new Response('hello')
|
||||
@ -120,21 +154,21 @@ describe('Middleware', () => {
|
||||
describe('Custom 404', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const customNotFound = async (c, next) => {
|
||||
await next()
|
||||
if (c.res.status === 404) {
|
||||
c.res = new Response('Custom 404 Not Found', { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
app.notFound = () => {
|
||||
return new Response('Default 404 Nout Found', { status: 404 })
|
||||
}
|
||||
|
||||
app.use('*', customNotFound)
|
||||
app.use('*', async (c, next) => {
|
||||
await next()
|
||||
if (c.res.status === 404) {
|
||||
c.res = new Response('Custom 404 Not Found', { status: 404 })
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/hello', () => {
|
||||
return new Response('hello')
|
||||
})
|
||||
|
||||
it('Custom 404 Not Found', async () => {
|
||||
let req = new Request('/hello')
|
||||
let res = await app.dispatch(req)
|
||||
@ -146,15 +180,16 @@ describe('Custom 404', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
describe('Context', () => {
|
||||
const app = new Hono()
|
||||
app.get('/', (c) => {
|
||||
return c.text('get /')
|
||||
})
|
||||
|
||||
it('Middleware must be async function', () => {
|
||||
expect(() => {
|
||||
app.use('*', {})
|
||||
}).toThrow(TypeError)
|
||||
expect(() => {
|
||||
app.use('*', () => '')
|
||||
}).toThrow(TypeError)
|
||||
it('c.text', async () => {
|
||||
let req = new Request('/')
|
||||
let res = await app.dispatch(req)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('get /')
|
||||
})
|
||||
})
|
@ -1,12 +1,13 @@
|
||||
const { makeEdgeEnv } = require('edge-mock')
|
||||
const { Hono, Middleware } = require('../src/hono')
|
||||
import makeServiceWorkerEnv from 'service-worker-mock'
|
||||
import { Hono, Middleware } from '../src/hono'
|
||||
|
||||
makeEdgeEnv()
|
||||
declare var global: any
|
||||
Object.assign(global, makeServiceWorkerEnv())
|
||||
|
||||
describe('Builtin Middleware', () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', Middleware.poweredBy)
|
||||
app.use('*', Middleware.poweredBy())
|
||||
app.get('/', () => new Response('root'))
|
||||
|
||||
it('Builtin Powered By Middleware', async () => {
|
@ -1,4 +1,4 @@
|
||||
const Node = require('../src/node')
|
||||
import { Node } from '../src/node'
|
||||
|
||||
describe('Root Node', () => {
|
||||
const node = new Node()
|
||||
@ -11,7 +11,7 @@ describe('Root Node', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Root Node id not defined', () => {
|
||||
describe('Root Node is not defined', () => {
|
||||
const node = new Node()
|
||||
node.insert('get', '/hello', 'get hello')
|
||||
it('get /', () => {
|
||||
@ -57,8 +57,11 @@ describe('Basic Usage', () => {
|
||||
|
||||
describe('Name path', () => {
|
||||
const node = new Node()
|
||||
node.insert('get', '/entry/:id', 'get entry')
|
||||
node.insert('get', '/entry/:id/comment/:comment_id', 'get comment')
|
||||
node.insert('get', '/map/:location/events', 'get events')
|
||||
|
||||
it('get /entry/123', () => {
|
||||
node.insert('get', '/entry/:id', 'get entry')
|
||||
let res = node.search('get', '/entry/123')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get entry')
|
||||
@ -68,13 +71,11 @@ describe('Name path', () => {
|
||||
})
|
||||
|
||||
it('get /entry/456/comment', () => {
|
||||
node.insert('get', '/entry/:id', 'get entry')
|
||||
let res = node.search('get', '/entry/456/comment')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('get /entry/789/comment/123', () => {
|
||||
node.insert('get', '/entry/:id/comment/:comment_id', 'get comment')
|
||||
let res = node.search('get', '/entry/789/comment/123')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get comment')
|
||||
@ -83,7 +84,6 @@ describe('Name path', () => {
|
||||
})
|
||||
|
||||
it('get /map/:location/events', () => {
|
||||
node.insert('get', '/map/:location/events', 'get events')
|
||||
let res = node.search('get', '/map/yokohama/events')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get events')
|
||||
@ -93,30 +93,36 @@ describe('Name path', () => {
|
||||
|
||||
describe('Wildcard', () => {
|
||||
const node = new Node()
|
||||
node.insert('get', '/wildcard-abc/*/wildcard-efg', 'wildcard')
|
||||
it('/wildcard-abc/xxxxxx/wildcard-efg', () => {
|
||||
node.insert('get', '/wildcard-abc/*/wildcard-efg', 'wildcard')
|
||||
let res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('wildcard')
|
||||
})
|
||||
node.insert('get', '/wildcard-abc/*/wildcard-efg/hijk', 'wildcard')
|
||||
it('/wildcard-abc/xxxxxx/wildcard-efg/hijk', () => {
|
||||
let res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg/hijk')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('wildcard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Regexp', () => {
|
||||
const node = new Node()
|
||||
node.insert('get', '/regex-abc/:id{[0-9]+}/comment/:comment_id{[a-z]+}', 'regexp')
|
||||
it('/regexp-abc/123/comment/abc', () => {
|
||||
res = node.search('get', '/regex-abc/123/comment/abc')
|
||||
const res = node.search('get', '/regex-abc/123/comment/abc')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('regexp')
|
||||
expect(res.params['id']).toBe('123')
|
||||
expect(res.params['comment_id']).toBe('abc')
|
||||
})
|
||||
it('/regexp-abc/abc', () => {
|
||||
res = node.search('get', '/regex-abc/abc')
|
||||
const res = node.search('get', '/regex-abc/abc')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
it('/regexp-abc/123/comment/123', () => {
|
||||
res = node.search('get', '/regex-abc/123/comment/123')
|
||||
const res = node.search('get', '/regex-abc/123/comment/123')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -125,7 +131,7 @@ describe('All', () => {
|
||||
const node = new Node()
|
||||
node.insert('ALL', '/all-methods', 'all methods') // ALL
|
||||
it('/all-methods', () => {
|
||||
res = node.search('get', '/all-methods')
|
||||
let res = node.search('get', '/all-methods')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('all methods')
|
||||
res = node.search('put', '/all-methods')
|
||||
@ -133,3 +139,39 @@ describe('All', () => {
|
||||
expect(res.handler).toBe('all methods')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Special Wildcard', () => {
|
||||
const node = new Node()
|
||||
node.insert('ALL', '*', 'match all')
|
||||
|
||||
it('/foo', () => {
|
||||
let res = node.search('get', '/foo')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('match all')
|
||||
})
|
||||
it('/hello', () => {
|
||||
let res = node.search('get', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('match all')
|
||||
})
|
||||
it('/hello/foo', () => {
|
||||
let res = node.search('get', '/hello/foo')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('match all')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Special Wildcard deeply', () => {
|
||||
const node = new Node()
|
||||
node.insert('ALL', '/hello/*', 'match hello')
|
||||
it('/hello', () => {
|
||||
let res = node.search('get', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('match hello')
|
||||
})
|
||||
it('/hello/foo', () => {
|
||||
let res = node.search('get', '/hello/foo')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('match hello')
|
||||
})
|
||||
})
|
@ -1,88 +0,0 @@
|
||||
const { Hono } = require('../src/hono')
|
||||
|
||||
describe('Basic Usage', () => {
|
||||
const router = new Hono()
|
||||
|
||||
it('get, post hello', async () => {
|
||||
router.get('/hello', 'get hello')
|
||||
router.post('/hello', 'post hello')
|
||||
|
||||
let res = await router.matchRoute('GET', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('get hello')
|
||||
|
||||
res = await router.matchRoute('POST', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('post hello')
|
||||
|
||||
res = await router.matchRoute('PUT', '/hello')
|
||||
expect(res).toBeNull()
|
||||
|
||||
res = await router.matchRoute('GET', '/')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Complex', () => {
|
||||
let router = new Hono()
|
||||
|
||||
it('Named Param', async () => {
|
||||
router.get('/entry/:id', 'get entry')
|
||||
res = await router.matchRoute('GET', '/entry/123')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('get entry')
|
||||
expect(res.params['id']).toBe('123')
|
||||
})
|
||||
|
||||
it('Wildcard', async () => {
|
||||
router.get('/wild/*/card', 'get wildcard')
|
||||
res = await router.matchRoute('GET', '/wild/xxx/card')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('get wildcard')
|
||||
})
|
||||
|
||||
it('Regexp', async () => {
|
||||
router.get('/post/:date{[0-9]+}/:title{[a-z]+}', 'get post')
|
||||
res = await router.matchRoute('GET', '/post/20210101/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('get post')
|
||||
expect(res.params['date']).toBe('20210101')
|
||||
expect(res.params['title']).toBe('hello')
|
||||
res = await router.matchRoute('GET', '/post/onetwothree')
|
||||
expect(res).toBeNull()
|
||||
res = await router.matchRoute('GET', '/post/123/123')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Chained Route', () => {
|
||||
let router = new Hono()
|
||||
|
||||
it('Return rooter object', async () => {
|
||||
router = router.patch('/hello', 'patch hello')
|
||||
expect(router).not.toBeNull()
|
||||
router = router.delete('/hello', 'delete hello')
|
||||
res = await router.matchRoute('DELETE', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('delete hello')
|
||||
})
|
||||
|
||||
it('Chain with route method', async () => {
|
||||
router.route('/api/book').get('get book').post('post book').put('put book')
|
||||
|
||||
res = await router.matchRoute('GET', '/api/book')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('get book')
|
||||
|
||||
res = await router.matchRoute('POST', '/api/book')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('post book')
|
||||
|
||||
res = await router.matchRoute('PUT', '/api/book')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler[0]).toBe('put book')
|
||||
|
||||
res = await router.matchRoute('DELETE', '/api/book')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
56
test/router.test.ts
Normal file
56
test/router.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Router } from '../src/hono'
|
||||
|
||||
describe('Basic Usage', () => {
|
||||
const router = new Router<string>()
|
||||
|
||||
router.add('GET', '/hello', 'get hello')
|
||||
router.add('POST', '/hello', 'post hello')
|
||||
|
||||
it('get, post hello', async () => {
|
||||
let res = router.match('GET', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get hello')
|
||||
|
||||
res = router.match('POST', '/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('post hello')
|
||||
|
||||
res = router.match('PUT', '/hello')
|
||||
expect(res).toBeNull()
|
||||
|
||||
res = router.match('GET', '/')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Complex', () => {
|
||||
const router = new Router<string>()
|
||||
|
||||
it('Named Param', async () => {
|
||||
router.add('GET', '/entry/:id', 'get entry')
|
||||
let res = router.match('GET', '/entry/123')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get entry')
|
||||
expect(res.params['id']).toBe('123')
|
||||
})
|
||||
|
||||
it('Wildcard', async () => {
|
||||
router.add('GET', '/wild/*/card', 'get wildcard')
|
||||
let res = router.match('GET', '/wild/xxx/card')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get wildcard')
|
||||
})
|
||||
|
||||
it('Regexp', async () => {
|
||||
router.add('GET', '/post/:date{[0-9]+}/:title{[a-z]+}', 'get post')
|
||||
let res = router.match('GET', '/post/20210101/hello')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.handler).toBe('get post')
|
||||
expect(res.params['date']).toBe('20210101')
|
||||
expect(res.params['title']).toBe('hello')
|
||||
res = router.match('GET', '/post/onetwothree')
|
||||
expect(res).toBeNull()
|
||||
res = router.match('GET', '/post/123/123')
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
const { splitPath, getPattern, getPathFromURL } = require('../src/util')
|
||||
import { splitPath, getPattern, getPathFromURL } from '../src/util'
|
||||
|
||||
describe('Utility methods', () => {
|
||||
it('splitPath', () => {
|
||||
@ -20,6 +20,7 @@ describe('Utility methods', () => {
|
||||
|
||||
it('getPattern', () => {
|
||||
let res = getPattern(':id')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res[0]).toBe('id')
|
||||
expect(res[1]).toBe('(.+)')
|
||||
res = getPattern(':id{[0-9]+}')
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": false,
|
||||
"types": [
|
||||
"jest",
|
||||
"@cloudflare/workers-types"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user