Comparing version 1.1.1 to 1.2.0
@@ -1,2 +0,2 @@ | ||
import type { ErrorHandler } from './hono'; | ||
export declare const compose: <C>(middleware: Function[], onError?: ErrorHandler) => (context: C) => Promise<C>; | ||
import type { ErrorHandler, NotFoundHandler } from './hono'; | ||
export declare const compose: <C>(middleware: Function[], onError?: ErrorHandler, onNotFound?: NotFoundHandler) => (context: C, next?: Function) => Promise<C>; |
@@ -6,17 +6,27 @@ "use strict"; | ||
// Based on the code in the MIT licensed `koa-compose` package. | ||
const compose = (middleware, onError) => { | ||
return function (context) { | ||
const compose = (middleware, onError, onNotFound) => { | ||
return async (context, next) => { | ||
let index = -1; | ||
return dispatch(0); | ||
async function dispatch(i) { | ||
if (i === middleware.length) { | ||
return context; | ||
} | ||
if (i <= index) { | ||
return Promise.reject(new Error('next() called multiple times')); | ||
} | ||
const handler = middleware[i]; | ||
let handler = middleware[i]; | ||
index = i; | ||
if (i === middleware.length) | ||
handler = next; | ||
if (handler === undefined) { | ||
if (context instanceof context_1.Context && context.res === undefined) { | ||
context.res = onNotFound(context); | ||
} | ||
return Promise.resolve(context); | ||
} | ||
return Promise.resolve(handler(context, dispatch.bind(null, i + 1))) | ||
.then(() => { | ||
.then(async (res) => { | ||
// If handler return Response like `return c.text('foo')` | ||
if (res && context instanceof context_1.Context) { | ||
context.res = res; | ||
dispatch(i + 1); // <--- Call next() | ||
} | ||
return context; | ||
@@ -26,3 +36,5 @@ }) | ||
if (onError && context instanceof context_1.Context) { | ||
context.res = onError(err, context); | ||
if (err instanceof Error) { | ||
context.res = onError(err, context); | ||
} | ||
return context; | ||
@@ -29,0 +41,0 @@ } |
@@ -7,3 +7,2 @@ /// <reference types="@cloudflare/workers-types" /> | ||
export declare class Context<RequestParamKeyType = string, E = Env> { | ||
#private; | ||
req: Request<RequestParamKeyType>; | ||
@@ -13,2 +12,7 @@ res: Response; | ||
event: FetchEvent; | ||
private _headers; | ||
private _status; | ||
private _statusText; | ||
private _pretty; | ||
private _prettySpace; | ||
render: (template: string, params?: object, options?: object) => Promise<Response>; | ||
@@ -15,0 +19,0 @@ notFound: () => Response | Promise<Response>; |
"use strict"; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var _Context_headers, _Context_status, _Context_statusText, _Context_pretty, _Context_prettySpace; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -20,10 +8,10 @@ exports.Context = void 0; | ||
constructor(req, opts) { | ||
_Context_headers.set(this, void 0); | ||
_Context_status.set(this, void 0); | ||
_Context_statusText.set(this, void 0); | ||
_Context_pretty.set(this, void 0); | ||
_Context_prettySpace.set(this, 2); | ||
this.res = undefined; | ||
this._headers = {}; | ||
this._status = 200; | ||
this._statusText = ''; | ||
this._pretty = false; | ||
this._prettySpace = 2; | ||
this.req = this.initRequest(req); | ||
Object.assign(this, opts); | ||
__classPrivateFieldSet(this, _Context_headers, {}, "f"); | ||
} | ||
@@ -44,24 +32,20 @@ initRequest(req) { | ||
} | ||
__classPrivateFieldGet(this, _Context_headers, "f")[name] = value; | ||
this._headers[name] = value; | ||
} | ||
status(status) { | ||
if (this.res) { | ||
console.warn('c.res.status is already set.'); | ||
return; | ||
} | ||
__classPrivateFieldSet(this, _Context_status, status, "f"); | ||
__classPrivateFieldSet(this, _Context_statusText, (0, http_status_1.getStatusText)(status), "f"); | ||
this._status = status; | ||
this._statusText = (0, http_status_1.getStatusText)(status); | ||
} | ||
pretty(prettyJSON, space = 2) { | ||
__classPrivateFieldSet(this, _Context_pretty, prettyJSON, "f"); | ||
__classPrivateFieldSet(this, _Context_prettySpace, space, "f"); | ||
this._pretty = prettyJSON; | ||
this._prettySpace = space; | ||
} | ||
newResponse(data, init = {}) { | ||
init.status = init.status || __classPrivateFieldGet(this, _Context_status, "f") || 200; | ||
init.status = init.status || this._status || 200; | ||
init.statusText = | ||
init.statusText || __classPrivateFieldGet(this, _Context_statusText, "f") || (0, http_status_1.getStatusText)(init.status); | ||
init.headers = Object.assign(Object.assign({}, __classPrivateFieldGet(this, _Context_headers, "f")), init.headers); | ||
init.statusText || this._statusText || (0, http_status_1.getStatusText)(init.status); | ||
init.headers = Object.assign(Object.assign({}, this._headers), init.headers); | ||
return new Response(data, init); | ||
} | ||
body(data, status = __classPrivateFieldGet(this, _Context_status, "f"), headers = __classPrivateFieldGet(this, _Context_headers, "f")) { | ||
body(data, status = this._status, headers = this._headers) { | ||
return this.newResponse(data, { | ||
@@ -72,3 +56,3 @@ status: status, | ||
} | ||
text(text, status = __classPrivateFieldGet(this, _Context_status, "f"), headers = {}) { | ||
text(text, status = this._status, headers = {}) { | ||
if (typeof text !== 'string') { | ||
@@ -80,8 +64,8 @@ throw new TypeError('text method arg must be a string!'); | ||
} | ||
json(object, status = __classPrivateFieldGet(this, _Context_status, "f"), headers = {}) { | ||
json(object, status = this._status, headers = {}) { | ||
if (typeof object !== 'object') { | ||
throw new TypeError('json method arg must be an object!'); | ||
} | ||
const body = __classPrivateFieldGet(this, _Context_pretty, "f") | ||
? JSON.stringify(object, null, __classPrivateFieldGet(this, _Context_prettySpace, "f")) | ||
const body = this._pretty | ||
? JSON.stringify(object, null, this._prettySpace) | ||
: JSON.stringify(object); | ||
@@ -91,3 +75,3 @@ headers['Content-Type'] || (headers['Content-Type'] = 'application/json; charset=UTF-8'); | ||
} | ||
html(html, status = __classPrivateFieldGet(this, _Context_status, "f"), headers = {}) { | ||
html(html, status = this._status, headers = {}) { | ||
if (typeof html !== 'string') { | ||
@@ -117,2 +101,1 @@ throw new TypeError('html method arg must be a string!'); | ||
exports.Context = Context; | ||
_Context_headers = new WeakMap(), _Context_status = new WeakMap(), _Context_statusText = new WeakMap(), _Context_pretty = new WeakMap(), _Context_prettySpace = new WeakMap(); |
@@ -5,2 +5,3 @@ /// <reference types="@cloudflare/workers-types" /> | ||
import type { Router } from './router'; | ||
import { METHOD_NAME_ALL_LOWERCASE } from './router'; | ||
declare global { | ||
@@ -13,5 +14,4 @@ interface Request<ParamKeyType = string> { | ||
} | ||
export declare type Handler<RequestParamKeyType = string, E = Env> = (c: Context<RequestParamKeyType, E>) => Response | Promise<Response>; | ||
export declare type MiddlewareHandler<E = Env> = (c: Context<string, E>, next: Next) => Promise<void>; | ||
export declare type NotFoundHandler<E = Env> = (c: Context<string, E>) => Response | Promise<Response>; | ||
export declare type Handler<RequestParamKeyType = string, E = Env> = (c: Context<RequestParamKeyType, E>, next?: Next) => Response | Promise<Response> | void | Promise<void>; | ||
export declare type NotFoundHandler<E = Env> = (c: Context<string, E>) => Response; | ||
export declare type ErrorHandler<E = Env> = (err: Error, c: Context<string, E>) => Response; | ||
@@ -22,20 +22,43 @@ export declare type Next = () => Promise<void>; | ||
declare type ParamKeys<Path> = Path extends `${infer Component}/${infer Rest}` ? ParamKey<Component> | ParamKeys<Rest> : ParamKey<Path>; | ||
interface HandlerInterface<T extends string, E = Env> { | ||
<Path extends string>(path: Path, handler: Handler<ParamKeys<Path>, E>): Hono<E, Path>; | ||
(path: string, handler: Handler<string, E>): Hono<E, T>; | ||
<Path extends T>(handler: Handler<ParamKeys<T>, E>): Hono<E, Path>; | ||
(handler: Handler<string, E>): Hono<E, T>; | ||
interface HandlerInterface<T extends string, E = Env, U = Hono<E, T>> { | ||
<Path extends string>(path: Path, ...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E>[]): U; | ||
(path: string, ...handlers: Handler<string, E>[]): U; | ||
<Path extends string>(...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E>[]): U; | ||
(...handlers: Handler<string, E>[]): U; | ||
} | ||
declare const Hono_base: new <E_1 extends Env, T extends string>() => { | ||
delete: HandlerInterface<T, E_1>; | ||
get: HandlerInterface<T, E_1>; | ||
post: HandlerInterface<T, E_1>; | ||
put: HandlerInterface<T, E_1>; | ||
head: HandlerInterface<T, E_1>; | ||
options: HandlerInterface<T, E_1>; | ||
patch: HandlerInterface<T, E_1>; | ||
all: HandlerInterface<T, E_1>; | ||
declare const methods: readonly ["get", "post", "put", "delete", "head", "options", "patch"]; | ||
declare type Methods = typeof methods[number] | typeof METHOD_NAME_ALL_LOWERCASE; | ||
interface Routing<E extends Env> { | ||
path: string; | ||
method: Methods; | ||
handler: Handler<string, E>; | ||
} | ||
declare const Route_base: new <E_1 extends Env, T extends string, U>() => { | ||
delete: HandlerInterface<T, E_1, U>; | ||
get: HandlerInterface<T, E_1, U>; | ||
all: HandlerInterface<T, E_1, U>; | ||
post: HandlerInterface<T, E_1, U>; | ||
put: HandlerInterface<T, E_1, U>; | ||
head: HandlerInterface<T, E_1, U>; | ||
options: HandlerInterface<T, E_1, U>; | ||
patch: HandlerInterface<T, E_1, U>; | ||
}; | ||
export declare class Hono<E = Env, P extends string = ''> extends Hono_base<E, P> { | ||
export declare class Route<E = Env, P extends string = ''> extends Route_base<E, P, Route<E, P>> { | ||
#private; | ||
routes: Routing<E>[]; | ||
constructor(); | ||
private add; | ||
} | ||
declare const Hono_base: new <E_1 extends Env, T extends string, U>() => { | ||
delete: HandlerInterface<T, E_1, U>; | ||
get: HandlerInterface<T, E_1, U>; | ||
all: HandlerInterface<T, E_1, U>; | ||
post: HandlerInterface<T, E_1, U>; | ||
put: HandlerInterface<T, E_1, U>; | ||
head: HandlerInterface<T, E_1, U>; | ||
options: HandlerInterface<T, E_1, U>; | ||
patch: HandlerInterface<T, E_1, U>; | ||
}; | ||
export declare class Hono<E = Env, P extends string = ''> extends Hono_base<E, P, Hono<E, P>> { | ||
#private; | ||
readonly routerClass: { | ||
@@ -49,5 +72,5 @@ new (): Router<any>; | ||
private errorHandler; | ||
route(path: string): Hono<E, P>; | ||
use(path: string, middleware: MiddlewareHandler<E>): Hono<E, P>; | ||
use(middleware: MiddlewareHandler<E>): Hono<E, P>; | ||
route(path: string, route?: Route): Hono<E, P>; | ||
use(path: string, ...middleware: Handler<string, E>[]): Hono<E, P>; | ||
use(...middleware: Handler<string, E>[]): Hono<E, P>; | ||
onError(handler: ErrorHandler<E>): Hono<E, P>; | ||
@@ -54,0 +77,0 @@ notFound(handler: NotFoundHandler<E>): Hono<E, P>; |
117
dist/hono.js
@@ -13,11 +13,12 @@ "use strict"; | ||
}; | ||
var _Hono_router, _Hono_middlewareRouters, _Hono_tempPath; | ||
var _Route_path, _Hono_router, _Hono_tempPath; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Hono = void 0; | ||
exports.Hono = exports.Route = void 0; | ||
const compose_1 = require("./compose"); | ||
const context_1 = require("./context"); | ||
const router_1 = require("./router"); | ||
const router_2 = require("./router"); | ||
const trie_router_1 = require("./router/trie-router"); // Default Router | ||
const url_1 = require("./utils/url"); | ||
const methods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch', 'all']; | ||
const methods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch']; | ||
function defineDynamicClass() { | ||
@@ -27,2 +28,33 @@ return class { | ||
} | ||
class Route extends defineDynamicClass() { | ||
constructor() { | ||
super(); | ||
this.routes = []; | ||
_Route_path.set(this, ''); | ||
const allMethods = [...methods, router_2.METHOD_NAME_ALL_LOWERCASE]; | ||
allMethods.map((method) => { | ||
this[method] = (args1, ...args) => { | ||
if (typeof args1 === 'string') { | ||
__classPrivateFieldSet(this, _Route_path, args1, "f"); | ||
} | ||
else { | ||
this.add(method, __classPrivateFieldGet(this, _Route_path, "f"), args1); | ||
} | ||
args.map((handler) => { | ||
if (typeof handler !== 'string') { | ||
this.add(method, __classPrivateFieldGet(this, _Route_path, "f"), handler); | ||
} | ||
}); | ||
return this; | ||
}; | ||
}); | ||
} | ||
add(method, path, handler) { | ||
const r = { path: path, method: method, handler: handler }; | ||
this.routes.push(r); | ||
return this; | ||
} | ||
} | ||
exports.Route = Route; | ||
_Route_path = new WeakMap(); | ||
class Hono extends defineDynamicClass() { | ||
@@ -34,3 +66,2 @@ constructor(init = {}) { | ||
_Hono_router.set(this, void 0); | ||
_Hono_middlewareRouters.set(this, void 0); | ||
_Hono_tempPath.set(this, void 0); | ||
@@ -46,9 +77,17 @@ this.notFoundHandler = (c) => { | ||
}; | ||
methods.map((method) => { | ||
this[method] = (arg1, arg2) => { | ||
if (typeof arg1 === 'string') { | ||
this.path = arg1; | ||
return this.addRoute(method, this.path, arg2); | ||
const allMethods = [...methods, router_2.METHOD_NAME_ALL_LOWERCASE]; | ||
allMethods.map((method) => { | ||
this[method] = (args1, ...args) => { | ||
if (typeof args1 === 'string') { | ||
this.path = args1; | ||
} | ||
return this.addRoute(method, this.path, arg1); | ||
else { | ||
this.addRoute(method, this.path, args1); | ||
} | ||
args.map((handler) => { | ||
if (typeof handler !== 'string') { | ||
this.addRoute(method, this.path, handler); | ||
} | ||
}); | ||
return this; | ||
}; | ||
@@ -58,26 +97,25 @@ }); | ||
__classPrivateFieldSet(this, _Hono_router, new this.routerClass(), "f"); | ||
__classPrivateFieldSet(this, _Hono_middlewareRouters, [], "f"); | ||
__classPrivateFieldSet(this, _Hono_tempPath, null, "f"); | ||
} | ||
route(path) { | ||
route(path, route) { | ||
const newHono = new Hono(); | ||
__classPrivateFieldSet(newHono, _Hono_tempPath, path, "f"); | ||
__classPrivateFieldSet(newHono, _Hono_router, __classPrivateFieldGet(this, _Hono_router, "f"), "f"); | ||
if (route) { | ||
route.routes.map((r) => { | ||
newHono.addRoute(r.method, r.path, r.handler); | ||
}); | ||
} | ||
return newHono; | ||
} | ||
use(arg1, arg2) { | ||
let handler; | ||
use(arg1, ...handlers) { | ||
if (typeof arg1 === 'string') { | ||
this.path = arg1; | ||
handler = arg2; | ||
} | ||
else { | ||
handler = arg1; | ||
handlers.unshift(arg1); | ||
} | ||
if (handler.constructor.name !== 'AsyncFunction') { | ||
throw new TypeError('middleware must be a async function!'); | ||
} | ||
const router = new this.routerClass(); | ||
router.add(router_1.METHOD_NAME_OF_ALL, this.path, handler); | ||
__classPrivateFieldGet(this, _Hono_middlewareRouters, "f").push(router); | ||
handlers.map((handler) => { | ||
this.addRoute(router_1.METHOD_NAME_ALL, this.path, handler); | ||
}); | ||
return this; | ||
@@ -99,3 +137,2 @@ } | ||
__classPrivateFieldGet(this, _Hono_router, "f").add(method, path, handler); | ||
return this; | ||
} | ||
@@ -109,3 +146,2 @@ async matchRoute(method, path) { | ||
const result = await this.matchRoute(method, path); | ||
// Methods for Request object | ||
request.param = (key) => { | ||
@@ -115,22 +151,17 @@ if (result) | ||
}; | ||
const handler = result ? result.handler : this.notFoundHandler; | ||
const middleware = []; | ||
for (const mr of __classPrivateFieldGet(this, _Hono_middlewareRouters, "f")) { | ||
const mwResult = mr.match(router_1.METHOD_NAME_OF_ALL, path); | ||
if (mwResult) | ||
middleware.push(mwResult.handler); | ||
const handlers = result ? result.handlers : [this.notFoundHandler]; | ||
const c = new context_1.Context(request, { env: env, event: event, res: undefined }); | ||
c.notFound = () => this.notFoundHandler(c); | ||
const composed = (0, compose_1.compose)(handlers, this.errorHandler, this.notFoundHandler); | ||
let context; | ||
try { | ||
context = await composed(c); | ||
} | ||
const wrappedHandler = async (context, next) => { | ||
const res = await handler(context); | ||
if (!(res instanceof Response)) { | ||
throw new TypeError('response must be a instance of Response'); | ||
catch (err) { | ||
if (err instanceof Error) { | ||
return this.errorHandler(err, c); | ||
} | ||
context.res = res; | ||
await next(); | ||
}; | ||
middleware.push(wrappedHandler); | ||
const composed = (0, compose_1.compose)(middleware, this.errorHandler); | ||
const c = new context_1.Context(request, { env: env, event: event, res: null }); | ||
c.notFound = () => this.notFoundHandler(c); | ||
const context = await composed(c); | ||
} | ||
if (!context.res) | ||
return context.notFound(); | ||
return context.res; | ||
@@ -155,2 +186,2 @@ } | ||
exports.Hono = Hono; | ||
_Hono_router = new WeakMap(), _Hono_middlewareRouters = new WeakMap(), _Hono_tempPath = new WeakMap(); | ||
_Hono_router = new WeakMap(), _Hono_tempPath = new WeakMap(); |
@@ -1,4 +0,4 @@ | ||
export { Hono } from './hono'; | ||
export type { Handler, MiddlewareHandler, Next } from './hono'; | ||
export { Hono, Route } from './hono'; | ||
export type { Handler, Next } from './hono'; | ||
export { Context } from './context'; | ||
export type { Env } from './context'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Context = exports.Hono = void 0; | ||
exports.Context = exports.Route = exports.Hono = void 0; | ||
var hono_1 = require("./hono"); | ||
Object.defineProperty(exports, "Hono", { enumerable: true, get: function () { return hono_1.Hono; } }); | ||
Object.defineProperty(exports, "Route", { enumerable: true, get: function () { return hono_1.Route; } }); | ||
var context_1 = require("./context"); | ||
Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return context_1.Context; } }); |
@@ -44,15 +44,16 @@ "use strict"; | ||
// Authorized OK | ||
return next(); | ||
await next(); | ||
} | ||
} | ||
} | ||
ctx.res = new Response('Unauthorized', { | ||
status: 401, | ||
headers: { | ||
'WWW-Authenticate': 'Basic realm="' + options.realm.replace(/"/g, '\\"') + '"', | ||
}, | ||
}); | ||
return; | ||
else { | ||
ctx.res = new Response('Unauthorized', { | ||
status: 401, | ||
headers: { | ||
'WWW-Authenticate': 'Basic realm="' + options.realm.replace(/"/g, '\\"') + '"', | ||
}, | ||
}); | ||
} | ||
}; | ||
}; | ||
exports.basicAuth = basicAuth; |
@@ -11,2 +11,3 @@ "use strict"; | ||
const credentials = ctx.req.headers.get('Authorization'); | ||
await next(); | ||
if (!credentials) { | ||
@@ -29,3 +30,2 @@ ctx.res = new Response('Unauthorized', { | ||
}); | ||
return; | ||
} | ||
@@ -40,15 +40,13 @@ let authorized = false; | ||
} | ||
if (authorized) { | ||
return next(); | ||
if (!authorized) { | ||
ctx.res = new Response('Unauthorized', { | ||
status: 401, | ||
statusText: msg, | ||
headers: { | ||
'WWW-Authenticate': 'Bearer ${options.secret}', | ||
}, | ||
}); | ||
} | ||
ctx.res = new Response('Unauthorized', { | ||
status: 401, | ||
statusText: msg, | ||
headers: { | ||
'WWW-Authenticate': 'Bearer ${options.secret}', | ||
}, | ||
}); | ||
return; | ||
}; | ||
}; | ||
exports.jwt = jwt; |
@@ -1,2 +0,3 @@ | ||
export declare const METHOD_NAME_OF_ALL = "ALL"; | ||
export declare const METHOD_NAME_ALL: "ALL"; | ||
export declare const METHOD_NAME_ALL_LOWERCASE: "all"; | ||
export declare abstract class Router<T> { | ||
@@ -7,5 +8,5 @@ abstract add(method: string, path: string, handler: T): void; | ||
export declare class Result<T> { | ||
handler: T; | ||
handlers: T[]; | ||
params: Record<string, string>; | ||
constructor(handler: T, params: Record<string, string>); | ||
constructor(handlers: T[], params: Record<string, string>); | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Result = exports.Router = exports.METHOD_NAME_OF_ALL = void 0; | ||
exports.METHOD_NAME_OF_ALL = 'ALL'; | ||
exports.Result = exports.Router = exports.METHOD_NAME_ALL_LOWERCASE = exports.METHOD_NAME_ALL = void 0; | ||
exports.METHOD_NAME_ALL = 'ALL'; | ||
exports.METHOD_NAME_ALL_LOWERCASE = 'all'; | ||
class Router { | ||
@@ -9,4 +10,4 @@ } | ||
class Result { | ||
constructor(handler, params) { | ||
this.handler = handler; | ||
constructor(handlers, params) { | ||
this.handlers = handlers; | ||
this.params = params; | ||
@@ -13,0 +14,0 @@ } |
@@ -9,4 +9,7 @@ export declare type ParamMap = Array<[string, number]>; | ||
children: Record<string, Node>; | ||
reverse: boolean; | ||
constructor({ reverse }?: Partial<Node>); | ||
newChildNode(): Node; | ||
insert(tokens: readonly string[], index: number, paramMap: ParamMap, context: Context): void; | ||
buildRegExpStr(): string; | ||
} |
@@ -38,5 +38,9 @@ "use strict"; | ||
class Node { | ||
constructor() { | ||
constructor({ reverse } = { reverse: false }) { | ||
this.children = {}; | ||
this.reverse = reverse; | ||
} | ||
newChildNode() { | ||
return new Node({ reverse: this.reverse }); | ||
} | ||
insert(tokens, index, paramMap, context) { | ||
@@ -62,3 +66,3 @@ var _a; | ||
if (!node) { | ||
node = this.children[regexpStr] = new Node(); | ||
node = this.children[regexpStr] = this.newChildNode(); | ||
if (name !== '') { | ||
@@ -73,3 +77,3 @@ node.varIndex = context.varIndex++; | ||
else { | ||
node = (_a = this.children)[token] || (_a[token] = new Node()); | ||
node = (_a = this.children)[token] || (_a[token] = this.newChildNode()); | ||
} | ||
@@ -79,5 +83,7 @@ node.insert(restTokens, index, paramMap, context); | ||
buildRegExpStr() { | ||
const strList = Object.keys(this.children) | ||
.sort(compareKey) | ||
.map((k) => { | ||
let childKeys = Object.keys(this.children).sort(compareKey); | ||
if (this.reverse) { | ||
childKeys = childKeys.reverse(); | ||
} | ||
const strList = childKeys.map((k) => { | ||
const c = this.children[k]; | ||
@@ -87,3 +93,3 @@ return (typeof c.varIndex === 'number' ? `(${k})@${c.varIndex}` : k) + c.buildRegExpStr(); | ||
if (typeof this.index === 'number') { | ||
strList.push(`#${this.index}`); | ||
strList.unshift(`#${this.index}`); | ||
} | ||
@@ -90,0 +96,0 @@ if (strList.length === 0) { |
import { Router, Result } from '../../router'; | ||
declare type Route<T> = [string, T]; | ||
interface Hint { | ||
components: string[]; | ||
regExpComponents: Array<true | string>; | ||
componentsLength: number; | ||
endWithWildcard: boolean; | ||
paramIndexList: number[]; | ||
maybeHandler: boolean; | ||
namedParams: [number, string, string][]; | ||
} | ||
interface Route<T> { | ||
method: string; | ||
path: string; | ||
hint: Hint; | ||
handlers: T[]; | ||
middleware: T[]; | ||
paramAliasMap: Record<string, string[]>; | ||
} | ||
export declare class RegExpRouter<T> extends Router<T> { | ||
routes?: Record<string, Route<T>[]>; | ||
routeData?: { | ||
routes: Route<T>[]; | ||
methods: Set<string>; | ||
}; | ||
add(method: string, path: string, handler: T): void; | ||
@@ -6,0 +25,0 @@ match(method: string, path: string): Result<T> | null; |
@@ -6,110 +6,330 @@ "use strict"; | ||
const trie_1 = require("../../router/reg-exp-router/trie"); | ||
const regExpMatchAll = new RegExp(''); | ||
const emptyParam = {}; | ||
const nullMatcher = [/^$/, []]; | ||
function initHint(path) { | ||
const components = path.match(/\/(?::\w+{[^}]+}|[^\/]*)/g) || []; | ||
let componentsLength = components.length; | ||
const paramIndexList = []; | ||
const regExpComponents = []; | ||
const namedParams = []; | ||
for (let i = 0, len = components.length; i < len; i++) { | ||
if (i === len - 1 && components[i] === '/*') { | ||
componentsLength--; | ||
break; | ||
} | ||
const m = components[i].match(/^\/:(\w+)({[^}]+})?/); | ||
if (m) { | ||
namedParams.push([i, m[1], m[2] || '[^/]+']); | ||
regExpComponents[i] = m[2] || true; | ||
} | ||
else if (components[i] === '/*') { | ||
regExpComponents[i] = true; | ||
} | ||
else { | ||
regExpComponents[i] = components[i]; | ||
} | ||
if (/\/(?::|\*)/.test(components[i])) { | ||
paramIndexList.push(i); | ||
} | ||
} | ||
return { | ||
components, | ||
regExpComponents, | ||
componentsLength, | ||
endWithWildcard: path.endsWith('*'), | ||
paramIndexList, | ||
namedParams, | ||
maybeHandler: true, | ||
}; | ||
} | ||
function compareRoute(a, b) { | ||
if (a.path === '*') { | ||
return 1; | ||
} | ||
let i = 0; | ||
const len = a.hint.regExpComponents.length; | ||
for (; i < len; i++) { | ||
if (a.hint.regExpComponents[i] !== b.hint.regExpComponents[i]) { | ||
if (a.hint.regExpComponents[i] === true) { | ||
break; | ||
} | ||
return 0; | ||
} | ||
} | ||
// may be ambiguous | ||
for (; i < len; i++) { | ||
if (a.hint.regExpComponents[i] !== true && | ||
a.hint.regExpComponents[i] !== b.hint.regExpComponents[i]) { | ||
return 2; | ||
} | ||
} | ||
return i === b.hint.regExpComponents.length || a.hint.endWithWildcard ? 1 : 0; | ||
} | ||
function buildMatcherFromPreprocessedRoutes(routes, hasAmbiguous = false) { | ||
const trie = new trie_1.Trie({ reverse: hasAmbiguous }); | ||
const handlers = []; | ||
if (routes.length === 0) { | ||
return nullMatcher; | ||
} | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
const paramMap = trie.insert(routes[i].path, i); | ||
handlers[i] = [ | ||
[...routes[i].middleware, ...routes[i].handlers], | ||
Object.keys(paramMap).length !== 0 ? paramMap : null, | ||
]; | ||
} | ||
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp(); | ||
for (let i = 0, len = handlers.length; i < len; i++) { | ||
const paramMap = handlers[i][1]; | ||
if (paramMap) { | ||
for (let j = 0, len = paramMap.length; j < len; j++) { | ||
paramMap[j][1] = paramReplacementMap[paramMap[j][1]]; | ||
const aliasTo = routes[i].paramAliasMap[paramMap[j][0]]; | ||
if (aliasTo) { | ||
for (let k = 0, len = aliasTo.length; k < len; k++) { | ||
paramMap.push([aliasTo[k], paramMap[j][1]]); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
const handlerMap = []; | ||
// using `in` because indexReplacementMap is a sparse array | ||
for (const i in indexReplacementMap) { | ||
handlerMap[i] = handlers[indexReplacementMap[i]]; | ||
} | ||
return [regexp, handlerMap]; | ||
} | ||
function verifyDuplicateParam(routes) { | ||
const nameMap = {}; | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
const route = routes[i]; | ||
for (let k = 0, len = route.hint.namedParams.length; k < len; k++) { | ||
const [index, name] = route.hint.namedParams[k]; | ||
if (name in nameMap && index !== nameMap[name]) { | ||
return false; | ||
} | ||
else { | ||
nameMap[name] = index; | ||
} | ||
} | ||
const paramAliasMap = route.paramAliasMap; | ||
const paramAliasMapKeys = Object.keys(paramAliasMap); | ||
for (let k = 0, len = paramAliasMapKeys.length; k < len; k++) { | ||
const aliasFrom = paramAliasMapKeys[k]; | ||
for (let l = 0, len = paramAliasMap[aliasFrom].length; l < len; l++) { | ||
const aliasTo = paramAliasMap[aliasFrom][l]; | ||
const index = nameMap[aliasFrom]; | ||
if (aliasTo in nameMap && index !== nameMap[aliasTo]) { | ||
return false; | ||
} | ||
else { | ||
nameMap[aliasTo] = index; | ||
} | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
class RegExpRouter extends router_1.Router { | ||
constructor() { | ||
super(...arguments); | ||
this.routes = {}; | ||
this.routeData = { routes: [], methods: new Set() }; | ||
} | ||
add(method, path, handler) { | ||
var _a; | ||
if (!this.routes) { | ||
if (!this.routeData) { | ||
throw new Error('Can not add a route since the matcher is already built.'); | ||
} | ||
(_a = this.routes)[method] || (_a[method] = []); | ||
this.routes[method].push([path, handler]); | ||
const { routes, methods } = this.routeData; | ||
if (path === '/*') { | ||
path = '*'; | ||
} | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
if (routes[i].method === method && routes[i].path === path) { | ||
routes[i].handlers.push(handler); | ||
return; | ||
} | ||
} | ||
methods.add(method); | ||
routes.push({ | ||
method, | ||
path, | ||
handlers: [handler], | ||
hint: initHint(path), | ||
middleware: [], | ||
paramAliasMap: {}, | ||
}); | ||
} | ||
match(method, path) { | ||
const matchers = this.buildAllMatchers(); | ||
let match; | ||
// Optimization for middleware | ||
const methods = Object.keys(matchers); | ||
if (methods.length === 1 && methods[0] === router_1.METHOD_NAME_OF_ALL) { | ||
const [regexp, handlers] = matchers[router_1.METHOD_NAME_OF_ALL]; | ||
if (handlers.length === 1) { | ||
const result = new router_1.Result(handlers[0][0], emptyParam); | ||
if (regexp === regExpMatchAll) { | ||
match = () => result; | ||
const [primaryMatchers, secondaryMatchers, hasAmbiguous] = this.buildAllMatchers(); | ||
this.match = hasAmbiguous | ||
? (method, path) => { | ||
const matcher = primaryMatchers[method] || primaryMatchers[router_1.METHOD_NAME_ALL]; | ||
let match = path.match(matcher[0]); | ||
if (!match) { | ||
// do not support secondary matchers here. | ||
return null; | ||
} | ||
else if (handlers.length === 1 && !handlers[0][1]) { | ||
match = (_, path) => (regexp.test(path) ? result : null); | ||
const params = {}; | ||
const handlers = new Set(); | ||
let regExpSrc = matcher[0].source; | ||
while (match) { | ||
let index = match.indexOf('', 1); | ||
for (;;) { | ||
const [handler, paramMap] = matcher[1][index]; | ||
if (paramMap) { | ||
for (let i = 0, len = paramMap.length; i < len; i++) { | ||
params[paramMap[i][0]] = match[paramMap[i][1]]; | ||
} | ||
} | ||
for (let i = 0, len = handler.length; i < len; i++) { | ||
handlers.add(handler[i]); | ||
} | ||
const newIndex = match.indexOf('', index + 1); | ||
if (newIndex === -1) { | ||
break; | ||
} | ||
index = newIndex; | ||
} | ||
regExpSrc = regExpSrc.replace(new RegExp(`((?:(?:\\(\\?:|.)*?\\([^)]*\\)){${index - 1}}.*?)\\(\\)`), '$1(^)'); | ||
match = path.match(new RegExp(regExpSrc)); | ||
} | ||
return new router_1.Result([...handlers.values()], params); | ||
} | ||
} | ||
match || (match = (method, path) => { | ||
const matcher = matchers[method] || matchers[router_1.METHOD_NAME_OF_ALL]; | ||
if (!matcher) { | ||
return null; | ||
: (method, path) => { | ||
let matcher = primaryMatchers[method] || primaryMatchers[router_1.METHOD_NAME_ALL]; | ||
let match = path.match(matcher[0]); | ||
if (!match) { | ||
const matchers = secondaryMatchers[method] || secondaryMatchers[router_1.METHOD_NAME_ALL]; | ||
for (let i = 0, len = matchers.length; i < len && !match; i++) { | ||
matcher = matchers[i]; | ||
match = path.match(matcher[0]); | ||
} | ||
if (!match) { | ||
return null; | ||
} | ||
} | ||
const index = match.indexOf('', 1); | ||
const [handler, paramMap] = matcher[1][index]; | ||
if (!paramMap) { | ||
return new router_1.Result(handler, emptyParam); | ||
} | ||
const params = {}; | ||
for (let i = 0, len = paramMap.length; i < len; i++) { | ||
params[paramMap[i][0]] = match[paramMap[i][1]]; | ||
} | ||
return new router_1.Result(handler, params); | ||
}; | ||
return this.match(method, path); | ||
} | ||
buildAllMatchers() { | ||
this.routeData.routes.sort(({ hint: a }, { hint: b }) => { | ||
if (a.componentsLength !== b.componentsLength) { | ||
return a.componentsLength - b.componentsLength; | ||
} | ||
const match = path.match(matcher[0]); | ||
if (!match) { | ||
return null; | ||
for (let i = 0, len = Math.min(a.paramIndexList.length, b.paramIndexList.length) + 1; i < len; i++) { | ||
if (a.paramIndexList[i] !== b.paramIndexList[i]) { | ||
if (a.paramIndexList[i] === undefined) { | ||
return -1; | ||
} | ||
else if (b.paramIndexList[i] === undefined) { | ||
return 1; | ||
} | ||
else { | ||
return a.paramIndexList[i] - b.paramIndexList[i]; | ||
} | ||
} | ||
} | ||
const index = match.indexOf('', 1); | ||
const [handler, paramMap] = matcher[1][index]; | ||
if (!paramMap) { | ||
return new router_1.Result(handler, emptyParam); | ||
if (a.endWithWildcard !== b.endWithWildcard) { | ||
return a.endWithWildcard ? -1 : 1; | ||
} | ||
const params = {}; | ||
for (let i = 0; i < paramMap.length; i++) { | ||
params[paramMap[i][0]] = match[paramMap[i][1]]; | ||
} | ||
return new router_1.Result(handler, params); | ||
return 0; | ||
}); | ||
this.match = match; | ||
return this.match(method, path); | ||
} | ||
buildAllMatchers() { | ||
const matchers = {}; | ||
Object.keys(this.routes).forEach((method) => { | ||
matchers[method] = this.buildMatcher(method); | ||
const primaryMatchers = {}; | ||
const secondaryMatchers = {}; | ||
let hasAmbiguous = false; | ||
this.routeData.methods.forEach((method) => { | ||
let _hasAmbiguous; | ||
[primaryMatchers[method], secondaryMatchers[method], _hasAmbiguous] = | ||
this.buildMatcher(method); | ||
hasAmbiguous = hasAmbiguous || _hasAmbiguous; | ||
}); | ||
delete this.routes; // to reduce memory usage | ||
return matchers; | ||
primaryMatchers[router_1.METHOD_NAME_ALL] || (primaryMatchers[router_1.METHOD_NAME_ALL] = nullMatcher); | ||
secondaryMatchers[router_1.METHOD_NAME_ALL] || (secondaryMatchers[router_1.METHOD_NAME_ALL] = []); | ||
delete this.routeData; // to reduce memory usage | ||
return [primaryMatchers, secondaryMatchers, hasAmbiguous]; | ||
} | ||
buildMatcher(method) { | ||
const trie = new trie_1.Trie(); | ||
const handlers = []; | ||
const targetMethods = [method]; | ||
if (method !== router_1.METHOD_NAME_OF_ALL) { | ||
targetMethods.unshift(router_1.METHOD_NAME_OF_ALL); | ||
var _a, _b; | ||
let hasAmbiguous = false; | ||
const targetMethods = new Set([method, router_1.METHOD_NAME_ALL]); | ||
const routes = this.routeData.routes.filter(({ method }) => targetMethods.has(method)); | ||
// Reset temporary data per method | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
routes[i].middleware = []; | ||
routes[i].paramAliasMap = {}; | ||
} | ||
const routes = targetMethods.flatMap((method) => this.routes[method] || []); | ||
if (routes.length === 0) { | ||
return null; | ||
} | ||
if (method === router_1.METHOD_NAME_OF_ALL) { | ||
if (routes.length === 1 && routes[0][0] === '*') { | ||
return [regExpMatchAll, [[routes[0][1], null]]]; | ||
// preprocess routes | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
for (let j = i + 1; j < len; j++) { | ||
const compareResult = compareRoute(routes[i], routes[j]); | ||
// i includes j | ||
if (compareResult === 1) { | ||
const components = routes[j].hint.components; | ||
const namedParams = routes[i].hint.namedParams; | ||
for (let k = 0, len = namedParams.length; k < len; k++) { | ||
const c = components[namedParams[k][0]]; | ||
const m = c.match(/^\/:(\w+)({[^}]+})?/); | ||
if (m && namedParams[k][1] === m[1]) { | ||
continue; | ||
} | ||
if (m) { | ||
(_a = routes[j].paramAliasMap)[_b = m[1]] || (_a[_b] = []); | ||
routes[j].paramAliasMap[m[1]].push(namedParams[k][1]); | ||
} | ||
else { | ||
components[namedParams[k][0]] = `/:${namedParams[k][1]}{${c.substring(1)}}`; | ||
routes[j].hint.namedParams.push([ | ||
namedParams[k][0], | ||
namedParams[k][1], | ||
c.substring(1), | ||
]); | ||
routes[j].path = components.join(''); | ||
} | ||
} | ||
routes[j].middleware.push(...routes[i].handlers); | ||
routes[i].hint.maybeHandler = false; | ||
} | ||
else if (compareResult === 2) { | ||
// ambiguous | ||
hasAmbiguous = true; | ||
if (!verifyDuplicateParam([routes[i], routes[j]])) { | ||
throw new Error('Duplicate param name'); | ||
} | ||
} | ||
} | ||
if (routes.length === 1 && !routes[0][0].match(/:/)) { | ||
// there is only one route and no capture | ||
const tmp = routes[0][0].endsWith('*') | ||
? routes[0][0].replace(/\/\*$/, '(?:$|/)') // /path/to/* => /path/to(?:$|/) | ||
: `${routes[0][0]}$`; // /path/to/action => /path/to/action$ | ||
const regExpStr = `^${tmp.replace(/\*/g, '[^/]+')}`; // /prefix/*/path/to => /prefix/[^/]+/path/to | ||
return [new RegExp(regExpStr), [[routes[0][1], null]]]; | ||
if (!verifyDuplicateParam([routes[i]])) { | ||
throw new Error('Duplicate param name'); | ||
} | ||
} | ||
for (let i = 0; i < routes.length; i++) { | ||
const paramMap = trie.insert(routes[i][0], i); | ||
handlers[i] = [routes[i][1], Object.keys(paramMap).length !== 0 ? paramMap : null]; | ||
if (hasAmbiguous) { | ||
return [buildMatcherFromPreprocessedRoutes(routes, hasAmbiguous), [], hasAmbiguous]; | ||
} | ||
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp(); | ||
for (let i = 0; i < handlers.length; i++) { | ||
const paramMap = handlers[i][1]; | ||
if (paramMap) { | ||
for (let i = 0; i < paramMap.length; i++) { | ||
paramMap[i][1] = paramReplacementMap[paramMap[i][1]]; | ||
} | ||
const primaryRoutes = []; | ||
const secondaryRoutes = []; | ||
for (let i = 0, len = routes.length; i < len; i++) { | ||
if (routes[i].hint.maybeHandler || !routes[i].hint.endWithWildcard) { | ||
primaryRoutes.push(routes[i]); | ||
} | ||
else { | ||
secondaryRoutes.push(routes[i]); | ||
} | ||
} | ||
const handlerMap = []; | ||
// using `in` because indexReplacementMap is a sparse array | ||
for (const i in indexReplacementMap) { | ||
handlerMap[i] = handlers[indexReplacementMap[i]]; | ||
} | ||
return [regexp, handlerMap]; | ||
return [ | ||
buildMatcherFromPreprocessedRoutes(primaryRoutes, hasAmbiguous), | ||
[buildMatcherFromPreprocessedRoutes(secondaryRoutes, hasAmbiguous)], | ||
hasAmbiguous, | ||
]; | ||
} | ||
} | ||
exports.RegExpRouter = RegExpRouter; |
@@ -5,7 +5,11 @@ import type { ParamMap, Context } from '../../router/reg-exp-router/node'; | ||
export declare type ReplacementMap = number[]; | ||
interface InitOptions { | ||
reverse: boolean; | ||
} | ||
export declare class Trie { | ||
context: Context; | ||
root: Node; | ||
constructor({ reverse }?: InitOptions); | ||
insert(path: string, index: number): ParamMap; | ||
buildRegExp(): [RegExp, ReplacementMap, ReplacementMap]; | ||
} |
@@ -6,5 +6,5 @@ "use strict"; | ||
class Trie { | ||
constructor() { | ||
constructor({ reverse } = { reverse: false }) { | ||
this.context = { varIndex: 0 }; | ||
this.root = new node_1.Node(); | ||
this.root = new node_1.Node({ reverse }); | ||
} | ||
@@ -11,0 +11,0 @@ insert(path, index) { |
import { Result } from '../../router'; | ||
import type { Pattern } from '../../utils/url'; | ||
export declare class Node<T> { | ||
method: Record<string, T>; | ||
handler: T; | ||
methods: Record<string, T>[]; | ||
handlers: T[]; | ||
children: Record<string, Node<T>>; | ||
middlewares: []; | ||
patterns: Pattern[]; | ||
constructor(method?: string, handler?: T, children?: Record<string, Node<T>>); | ||
insert(method: string, path: string, handler: T): Node<T>; | ||
private getHandlers; | ||
private next; | ||
search(method: string, path: string): Result<T>; | ||
} |
@@ -6,13 +6,25 @@ "use strict"; | ||
const url_1 = require("../../utils/url"); | ||
const noRoute = () => { | ||
return null; | ||
}; | ||
function findParam(node, name) { | ||
for (let i = 0, len = node.patterns.length; i < len; i++) { | ||
if (typeof node.patterns[i] === 'object' && node.patterns[i][1] === name) { | ||
return true; | ||
} | ||
} | ||
const nodes = Object.values(node.children); | ||
for (let i = 0, len = nodes.length; i < len; i++) { | ||
if (findParam(nodes[i], name)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
class Node { | ||
constructor(method, handler, children) { | ||
this.children = children || {}; | ||
this.method = {}; | ||
this.methods = []; | ||
if (method && handler) { | ||
this.method[method] = handler; | ||
const m = {}; | ||
m[method] = handler; | ||
this.methods = [m]; | ||
} | ||
this.middlewares = []; | ||
this.patterns = []; | ||
@@ -24,5 +36,10 @@ } | ||
const parts = (0, url_1.splitPath)(path); | ||
const parentPatterns = []; | ||
const errorMessage = (name) => { | ||
return `Duplicate param name, use another name instead of '${name}' - ${method} ${path} <--- '${name}'`; | ||
}; | ||
for (let i = 0, len = parts.length; i < len; i++) { | ||
const p = parts[i]; | ||
if (Object.keys(curNode.children).includes(p)) { | ||
parentPatterns.push(...curNode.patterns); | ||
curNode = curNode.children[p]; | ||
@@ -34,68 +51,119 @@ continue; | ||
if (pattern) { | ||
if (typeof pattern === 'object') { | ||
for (let j = 0, len = parentPatterns.length; j < len; j++) { | ||
if (typeof parentPatterns[j] === 'object' && parentPatterns[j][1] === pattern[1]) { | ||
throw new Error(errorMessage(pattern[1])); | ||
} | ||
} | ||
if (Object.values(curNode.children).some((n) => findParam(n, pattern[1]))) { | ||
throw new Error(errorMessage(pattern[1])); | ||
} | ||
} | ||
curNode.patterns.push(pattern); | ||
parentPatterns.push(...curNode.patterns); | ||
} | ||
parentPatterns.push(...curNode.patterns); | ||
curNode = curNode.children[p]; | ||
} | ||
curNode.method[method] = handler; | ||
if (!curNode.methods.length) { | ||
curNode.methods = []; | ||
} | ||
const m = {}; | ||
m[method] = handler; | ||
curNode.methods.push(m); | ||
return curNode; | ||
} | ||
getHandlers(node, method) { | ||
const handlers = []; | ||
node.methods.map((m) => { | ||
let handler = m[method]; | ||
if (handler !== undefined) { | ||
handlers.push(handler); | ||
return; | ||
} | ||
handler = m[router_1.METHOD_NAME_ALL]; | ||
if (handler !== undefined) { | ||
handlers.push(handler); | ||
return; | ||
} | ||
}); | ||
return handlers; | ||
} | ||
next(node, part, method, isLast) { | ||
const handlers = []; | ||
const nextNodes = []; | ||
const params = {}; | ||
for (let j = 0, len = node.patterns.length; j < len; j++) { | ||
const pattern = node.patterns[j]; | ||
// Wildcard | ||
// '/hello/*/foo' => match /hello/bar/foo | ||
if (pattern === '*') { | ||
const astNode = node.children['*']; | ||
if (astNode) { | ||
handlers.push(...this.getHandlers(astNode, method)); | ||
nextNodes.push(astNode); | ||
} | ||
} | ||
if (part === '') | ||
continue; | ||
// Named match | ||
// `/posts/:id` => match /posts/123 | ||
const [key, name, matcher] = pattern; | ||
if (matcher === true || (matcher instanceof RegExp && matcher.test(part))) { | ||
if (typeof key === 'string') { | ||
if (isLast === true) { | ||
handlers.push(...this.getHandlers(node.children[key], method)); | ||
} | ||
nextNodes.push(node.children[key]); | ||
} | ||
if (typeof name === 'string') { | ||
params[name] = part; | ||
} | ||
} | ||
} | ||
const nextNode = node.children[part]; | ||
if (nextNode) { | ||
if (isLast === true) { | ||
// '/hello/*' => match '/hello' | ||
if (nextNode.children['*'] !== undefined) { | ||
handlers.push(...this.getHandlers(nextNode.children['*'], method)); | ||
} | ||
handlers.push(...this.getHandlers(nextNode, method)); | ||
} | ||
nextNodes.push(nextNode); | ||
} | ||
const next = { | ||
nodes: nextNodes, | ||
handlers: handlers, | ||
params: params, | ||
}; | ||
return next; | ||
} | ||
search(method, path) { | ||
const handlers = []; | ||
let params = {}; | ||
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
let curNode = this; | ||
const params = {}; | ||
const curNode = this; | ||
let curNodes = [curNode]; | ||
const parts = (0, url_1.splitPath)(path); | ||
for (let i = 0, len = parts.length; i < len; i++) { | ||
const p = parts[i]; | ||
// '*' => match any path | ||
// /api/* => default wildcard match | ||
if (curNode.children['*'] && !curNode.children[p]) { | ||
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['*'])) { | ||
const isLast = i === len - 1; | ||
const tempNodes = []; | ||
for (let j = 0, len2 = curNodes.length; j < len2; j++) { | ||
const res = this.next(curNodes[j], p, method, isLast); | ||
if (res.nodes.length === 0) { | ||
continue; | ||
} | ||
handlers.push(...res.handlers); | ||
params = Object.assign(params, res.params); | ||
tempNodes.push(...res.nodes); | ||
} | ||
let isWildcard = false; | ||
let isParamMatch = false; | ||
for (let j = 0, len = curNode.patterns.length; j < len; j++) { | ||
const pattern = curNode.patterns[j]; | ||
// Wildcard | ||
// '/hello/*/foo' => match /hello/bar/foo | ||
if (pattern === '*') { | ||
curNode = curNode.children['*']; | ||
isWildcard = true; | ||
break; | ||
} | ||
// Named match | ||
const [key, name, matcher] = pattern; | ||
if (p !== '' && (matcher === true || matcher.test(p))) { | ||
params[name] = p; | ||
curNode = curNode.children[key]; | ||
isParamMatch = true; | ||
break; | ||
} | ||
return noRoute(); | ||
} | ||
if (isWildcard && i === len - 1) { | ||
break; | ||
} | ||
if (isWildcard === false && isParamMatch === false) { | ||
return noRoute(); | ||
} | ||
curNodes = tempNodes; | ||
} | ||
const handler = curNode.method[router_1.METHOD_NAME_OF_ALL] || curNode.method[method]; | ||
if (!handler) { | ||
return noRoute(); | ||
} | ||
return new router_1.Result(handler, params); | ||
if (handlers.length <= 0) | ||
return null; | ||
return new router_1.Result(handlers, params); | ||
} | ||
} | ||
exports.Node = Node; |
@@ -8,3 +8,3 @@ import { Router } from '../../router'; | ||
add(method: string, path: string, handler: T): void; | ||
match(method: string, path: string): Result<T> | null; | ||
match(method: string, path: string): Result<T>; | ||
} |
@@ -10,6 +10,6 @@ "use strict"; | ||
else if (contentType.includes('application/text')) { | ||
return r.text(); | ||
return await r.text(); | ||
} | ||
else if (contentType.startsWith('text')) { | ||
return r.text(); | ||
return await r.text(); | ||
} | ||
@@ -16,0 +16,0 @@ else if (contentType.includes('form')) { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.mergePath = exports.isAbsoluteURL = exports.getPathFromURL = exports.getPattern = exports.splitPath = void 0; | ||
const URL_REGEXP = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; | ||
const URL_REGEXP = /^https?:\/\/[a-zA-Z0-9\-\.:]+(\/?[^?#]*)/; | ||
const splitPath = (path) => { | ||
@@ -40,3 +40,3 @@ const paths = path.split(/\//); // faster than path.split('/') | ||
// default is true | ||
if (!params.strict && url.endsWith('/')) { | ||
if (params.strict === false && url.endsWith('/')) { | ||
url = url.slice(0, -1); | ||
@@ -46,3 +46,3 @@ } | ||
if (match) { | ||
return match[5]; | ||
return match[1]; | ||
} | ||
@@ -54,3 +54,3 @@ return ''; | ||
const match = url.match(URL_REGEXP); | ||
if (match && match[1]) { | ||
if (match) { | ||
return true; | ||
@@ -57,0 +57,0 @@ } |
{ | ||
"name": "hono", | ||
"version": "1.1.1", | ||
"version": "1.2.0", | ||
"description": "Ultrafast web framework for Cloudflare Workers.", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
243
README.md
@@ -9,8 +9,2 @@ <div align="center"> | ||
<p> | ||
<a href="https://github.com/honojs/hono/blob/master/README.md">English</a> | ||
· | ||
<a href="https://github.com/honojs/hono/blob/master/docs/README.ja.md">日本語</a> | ||
</p> | ||
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/honojs/hono/ci)](https://github.com/honojs/hono/actions) | ||
@@ -24,5 +18,5 @@ [![GitHub](https://img.shields.io/github/license/honojs/hono)](https://github.com/honojs/hono/blob/master/LICENSE) | ||
Hono - _**[炎] means flame🔥 in Japanese**_ - is a small, simple, and ultrafast web framework for Cloudflare Workers and Service Worker based serverless such as Fastly Compute@Edge. | ||
Hono - _**[炎] means flame🔥 in Japanese**_ - is a small, simple, and ultrafast web framework for Cloudflare Workers or Service Worker based serverless such as Fastly Compute@Edge. | ||
```js | ||
```ts | ||
import { Hono } from 'hono' | ||
@@ -39,6 +33,6 @@ const app = new Hono() | ||
- **Ultrafast** - the router does not use linear loops. | ||
- **Zero-dependencies** - using only Service Worker and Web standard API. | ||
- **Zero-dependencies** - using only Service Worker and Web Standard API. | ||
- **Middleware** - built-in middleware and ability to extend with your own middleware. | ||
- **TypeScript** - first-class TypeScript support. | ||
- **Optimized** - for Cloudflare Workers and Fastly Compute@Edge. | ||
- **Optimized** - for Cloudflare Workers. | ||
@@ -50,10 +44,18 @@ ## Benchmark | ||
```plain | ||
hono x 809,503 ops/sec ±6.94% (73 runs sampled) | ||
itty-router x 157,310 ops/sec ±4.31% (87 runs sampled) | ||
sunder x 328,350 ops/sec ±2.30% (95 runs sampled) | ||
worktop x 209,758 ops/sec ±4.28% (83 runs sampled) | ||
Fastest is hono | ||
✨ Done in 60.66s. | ||
hono - trie-router(default) x 737,602 ops/sec ±3.65% (67 runs sampled) | ||
hono - regexp-router x 1,188,203 ops/sec ±6.42% (60 runs sampled) | ||
itty-router x 163,970 ops/sec ±3.05% (91 runs sampled) | ||
sunder x 344,468 ops/sec ±0.87% (97 runs sampled) | ||
worktop x 222,044 ops/sec ±2.13% (85 runs sampled) | ||
Fastest is hono - regexp-router | ||
✨ Done in 84.04s. | ||
``` | ||
## Why so fast? | ||
Routers used in Hono are really smart. | ||
- **TrieRouter**(default) - Implemented with Trie tree structure. | ||
- **RegExpRouter** - Match routes with one big Regex made before dispatching at once. | ||
## Hono in 1 minute | ||
@@ -84,16 +86,43 @@ | ||
You can enable logger and CORS middleware with just this code. | ||
To enable logger and Etag middleware with just this code. | ||
```js | ||
```ts | ||
import { Hono } from 'hono' | ||
import { cors } from 'hono/cors' | ||
import { etag } from 'hono/etag' | ||
import { logger } from 'hono/logger' | ||
const app = new Hono() | ||
app.use('*', cors()).use(logger()) | ||
app.use('*', etag(), (logger()) | ||
``` | ||
And, the routing of Hono is so flexible. It's easy to construct large web applications. | ||
```ts | ||
import { Hono, Route } from 'hono' | ||
import { cors } from 'hono/cors' | ||
const app = new Hono() | ||
const v1 = new Route() | ||
v1.get('/posts', (c) => { | ||
return c.text('list pots') | ||
}) | ||
.post('/posts', cors(), (c) => { | ||
return c.text('created!', 201) | ||
}) | ||
.get('/posts/:id', (c) => { | ||
const id = c.req.param('id') | ||
return c.text(`your id is ${id}`) | ||
}) | ||
app.route('/v1', v1) | ||
``` | ||
### Web Standard | ||
Request and Response object used in Hono are extensions of the Web Standard [Fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API). If you are familiar with that, you don't need to know more than that. | ||
### Developer Experience | ||
And Hono provides fine _"**Developer Experience**"_. Easy access to Request/Response thanks to the `Context` object. | ||
Hono provides fine _"**Developer Experience**"_. Easy access to Request/Response thanks to the `Context` object. | ||
Above all, Hono is written in TypeScript. So, Hono has _"**Types**"_! | ||
@@ -110,8 +139,2 @@ | ||
```sh | ||
yarn add hono | ||
``` | ||
or | ||
```sh | ||
npm install hono | ||
@@ -124,5 +147,5 @@ ``` | ||
- app.**HTTP_METHOD**(\[path,\] handler) | ||
- app.**all**(\[path,\] handler) | ||
- app.**route**(path) | ||
- app.**HTTP_METHOD**(\[path,\] handler|middleware...) | ||
- app.**all**(\[path,\] handler|middleware...) | ||
- app.**route**(path, \[Route\]) | ||
- app.**use**(\[path,\] middleware) | ||
@@ -133,3 +156,3 @@ - app.**notFound**(handler) | ||
- app.**fetch**(request, env, event) | ||
- app.**request**(path, option) | ||
- app.**request**(path, options) | ||
@@ -140,3 +163,3 @@ ## Routing | ||
```js | ||
```ts | ||
// HTTP Methods | ||
@@ -157,3 +180,3 @@ app.get('/', (c) => c.text('GET /')) | ||
```js | ||
```ts | ||
app.get('/user/:name', (c) => { | ||
@@ -167,3 +190,3 @@ const name = c.req.param('name') | ||
```js | ||
```ts | ||
app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => { | ||
@@ -178,3 +201,3 @@ const date = c.req.param('date') | ||
```js | ||
```ts | ||
app | ||
@@ -192,15 +215,2 @@ .get('/endpoint', (c) => { | ||
### Nested route | ||
```js | ||
const book = app.route('/book') | ||
book.get('/', (c) => c.text('List Books')) // GET /book | ||
book.get('/:id', (c) => { | ||
// GET /book/:id | ||
const id = c.req.param('id') | ||
return c.text('Get Book: ' + id) | ||
}) | ||
book.post('/', (c) => c.text('Create Book')) // POST /book | ||
``` | ||
### no strict | ||
@@ -210,3 +220,3 @@ | ||
```js | ||
```ts | ||
const app = new Hono({ strict: false }) // Default is true | ||
@@ -226,4 +236,29 @@ | ||
## Route | ||
`Route` object enables Nested route. | ||
```ts | ||
const book = new Route() | ||
book.get('/', (c) => c.text('List Books')) // GET /book | ||
book.get('/:id', (c) => { | ||
// GET /book/:id | ||
const id = c.req.param('id') | ||
return c.text('Get Book: ' + id) | ||
}) | ||
book.post('/', (c) => c.text('Create Book')) // POST /book | ||
app.route('/book', book) | ||
``` | ||
## Middleware | ||
Middleware operate after/before executing Handler. We can get `Response` before dispatching or manipulate `Response` after dispatching. | ||
### Definition of Middleware | ||
- Handler - should return `Response` object. | ||
- Middleware - should return nothing, do `await next()` | ||
### Built-in Middleware | ||
@@ -233,3 +268,3 @@ | ||
```js | ||
```ts | ||
import { Hono } from 'hono' | ||
@@ -244,4 +279,2 @@ import { poweredBy } from 'hono/powered-by' | ||
app.use('*', logger()) | ||
// Or you can write: | ||
// app.use('*', poweredBy()).use(logger()) | ||
@@ -263,3 +296,3 @@ app.use( | ||
```js | ||
```ts | ||
// Custom logger | ||
@@ -303,7 +336,7 @@ app.use('*', async (c, next) => { | ||
To handle Request and Reponse, you can use `Context` object. | ||
To handle Request and Response, you can use `Context` object. | ||
### c.req | ||
```js | ||
```ts | ||
// Get Request object | ||
@@ -336,3 +369,3 @@ app.get('/hello', (c) => { | ||
```js | ||
```ts | ||
app.get('/welcome', (c) => { | ||
@@ -342,4 +375,6 @@ // Set headers | ||
c.header('Content-Type', 'text/plain') | ||
// Set HTTP status code | ||
c.status(201) | ||
// Return the response body | ||
@@ -352,3 +387,3 @@ return c.body('Thank you for comming') | ||
```js | ||
```ts | ||
new Response('Thank you for comming', { | ||
@@ -360,3 +395,2 @@ status: 201, | ||
'Content-Type': 'text/plain', | ||
'Content-Length': '22', | ||
}, | ||
@@ -370,3 +404,3 @@ }) | ||
```js | ||
```ts | ||
app.get('/say', (c) => { | ||
@@ -381,3 +415,3 @@ return c.text('Hello!') | ||
```js | ||
```ts | ||
app.get('/api', (c) => { | ||
@@ -392,3 +426,3 @@ return c.json({ message: 'Hello!' }) | ||
```js | ||
```ts | ||
app.get('/', (c) => { | ||
@@ -403,3 +437,3 @@ return c.html('<h1>Hello! Hono!</h1>') | ||
```js | ||
```ts | ||
app.get('/notfound', (c) => { | ||
@@ -414,3 +448,3 @@ return c.notFound() | ||
```js | ||
```ts | ||
app.get('/redirect', (c) => c.redirect('/')) | ||
@@ -422,3 +456,3 @@ app.get('/redirect-permanently', (c) => c.redirect('/', 301)) | ||
```js | ||
```ts | ||
// Response object | ||
@@ -433,3 +467,3 @@ app.use('/', (c, next) => { | ||
```js | ||
```ts | ||
// FetchEvent object | ||
@@ -446,3 +480,3 @@ app.use('*', async (c, next) => { | ||
```js | ||
```ts | ||
// Environment object for Cloudflare Workers | ||
@@ -459,3 +493,3 @@ app.get('*', async c => { | ||
```js | ||
```ts | ||
addEventListener('fetch', (event) => { | ||
@@ -470,3 +504,3 @@ event.respondWith(this.handleEvent(event)) | ||
```js | ||
```ts | ||
export default { | ||
@@ -477,7 +511,8 @@ fetch(request: Request, env: Env, event: FetchEvent) { | ||
} | ||
``` | ||
/* | ||
or just do: | ||
```ts | ||
export default app | ||
*/ | ||
``` | ||
@@ -498,57 +533,31 @@ | ||
Using [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/) or [Miniflare](https://miniflare.dev), you can develop the application locally and publish it with few commands. | ||
Using [Wrangler](https://developers.cloudflare.com/workers/cli-wrangler/), you can develop the application locally and publish it with few commands. | ||
Let's write your first code for Cloudflare Workers with Hono. | ||
--- | ||
### 1. `wrangler init` | ||
### Caution | ||
Initialize as a wrangler project. | ||
**Wrangler 1.x** does not support importing middleware. We recommend two ways: | ||
1. Use [Wragler 2.0 Beta](https://github.com/cloudflare/wrangler2). | ||
2. Build without webpack 4.x. For example, you can use esbuild. See [the starter template](https://github.com/honojs/hono-minimal). | ||
--- | ||
### 1. `npm init` | ||
Make a npm skeleton directory. | ||
```sh | ||
``` | ||
mkdir hono-example | ||
cd hono-example | ||
npm init -y | ||
npx wrangler init -y | ||
``` | ||
### 2. `wrangler init` | ||
### 2. `npm install hono` | ||
Initialize as a wrangler project. | ||
Install `hono` from the npm registry. | ||
```sh | ||
npx wrangler@beta init | ||
``` | ||
Answer the questions. If you want, you can answer `y`. | ||
``` | ||
Would you like to install wrangler into your package.json? (y/n) <--- n | ||
Would you like to use TypeScript? (y/n) <--- n | ||
Would you like to create a Worker at src/index.js? (y/n) <--- n | ||
``` | ||
### 3. `npm install hono` | ||
Install `hono` from the npm registry. | ||
```sh | ||
npm init -y | ||
npm i hono | ||
``` | ||
### 4. Write your app | ||
### 3. Write your app | ||
Only 4 lines!! | ||
Edit `src/index.ts`. Only 4 lines!! | ||
```js | ||
// index.js | ||
```ts | ||
// src/index.ts | ||
import { Hono } from 'hono' | ||
@@ -562,17 +571,17 @@ const app = new Hono() | ||
### 5. Run | ||
### 4. Run | ||
Run the development server locally. Then, access `http://127.0.0.1:8787/` in your Web browser. | ||
```sh | ||
npx wrangler@beta dev index.js | ||
``` | ||
npx wrangler dev | ||
``` | ||
### 6. Publish | ||
### 5. Publish | ||
Deploy to Cloudflare. That's all! | ||
```sh | ||
npx wrangler@beta publish index.js | ||
``` | ||
npx wrangler publish index.ts | ||
``` | ||
@@ -585,5 +594,5 @@ ## Starter template | ||
```sh | ||
wrangler generate my-app https://github.com/honojs/hono-minimal | ||
``` | ||
npx create-cloudflare my-app https://github.com/honojs/hono-minimal | ||
``` | ||
@@ -618,3 +627,3 @@ ## Examples | ||
Thanks to [all contributors](https://github.com/honojs/hono/graphs/contributors)! | ||
Thanks to [all contributors](https://github.com/honojs/hono/graphs/contributors)! Especially, [@metrue](https://github.com/metrue) and [@usualoma](https://github.com/usualoma)! | ||
@@ -621,0 +630,0 @@ ## Author |
118094
2686
599