From c47820d9c6d1dcf31dfe6f8440a7521893504c1c Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Sat, 8 Jun 2024 23:51:11 +0800 Subject: [PATCH 01/13] feat: support cjs and esm both BREAKING CHANGE: drop Node.js < 18.7.0 support - Drop generator function support - Drop Node.js < 18.7.0 support --- .eslintrc | 5 +- .github/workflows/nodejs.yml | 2 +- History.md => CHANGELOG.md | 0 README.md | 34 +++++- lib/utils.js | 18 --- package.json | 84 +++++++++----- {lib => src}/egg_router.js | 0 index.js => src/index.js | 0 lib/layer.js => src/layer.ts | 134 ++++++++++++++-------- {lib => src}/router.js | 0 src/types.ts | 4 + src/utils.ts | 19 +++ test/lib/{layer.test.js => layer.test.ts} | 19 ++- tsconfig.json | 10 ++ 14 files changed, 219 insertions(+), 110 deletions(-) rename History.md => CHANGELOG.md (100%) delete mode 100644 lib/utils.js rename {lib => src}/egg_router.js (100%) rename index.js => src/index.js (100%) rename lib/layer.js => src/layer.ts (59%) rename {lib => src}/router.js (100%) create mode 100644 src/types.ts create mode 100644 src/utils.ts rename test/lib/{layer.test.js => layer.test.ts} (96%) create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a59ba46..5a2a289 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,6 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest' - version: '8, 10, 12, 14, 16, 18, 20, 22' + version: '18, 20, 22' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/History.md b/CHANGELOG.md similarity index 100% rename from History.md rename to CHANGELOG.md diff --git a/README.md b/README.md index 3530802..7595ad4 100644 --- a/README.md +++ b/README.md @@ -27,20 +27,22 @@ Router core component for [Egg.js](https://github.com/eggjs). - [router.param(param, middleware) ⇒ Router](#routerparamparam-middleware--router) - [Router.url(path, params \[, options\]) ⇒ String](#routerurlpath-params--options--string) - [Tests](#tests) + - [Breaking changes on v3](#breaking-changes-on-v3) - [License](#license) <a name="exp_module_egg-router--Router"></a> ### Router ⏏ + **Kind**: Exported class <a name="new_module_egg-router--Router_new"></a> #### new Router([opts]) -Create a new router. +Create a new router. | Param | Type | Description | -| --- | --- | --- | +| --- | --- | --- | | [opts] | <code>Object</code> | | | [opts.prefix] | <code>String</code> | prefix router paths | @@ -62,9 +64,11 @@ app .use(router.routes()) .use(router.allowedMethods()); ``` + <a name="module_egg-router--Router+get|put|post|patch|delete|del"></a> #### router.get|put|post|patch|delete|del ⇒ <code>Router</code> + Create `router.verb()` methods, where *verb* is one of the HTTP verbs such as `router.get()` or `router.post()`. @@ -180,7 +184,7 @@ used to convert paths to regular expressions. **Kind**: instance property of <code>[Router](#exp_module_egg-router--Router)</code> | Param | Type | Description | -| --- | --- | --- | +| --- | --- | --- | | path | <code>String</code> | | | [middleware] | <code>function</code> | route middleware(s) | | callback | <code>function</code> | route callback | @@ -194,6 +198,7 @@ Returns router middleware which dispatches a route matching the request. <a name="module_egg-router--Router+use"></a> #### router.use([path], middleware) ⇒ <code>Router</code> + Use given middleware. Middleware run in the order they are defined by `.use()`. They are invoked @@ -209,6 +214,7 @@ sequentially, requests start at the first middleware and work their way | [...] | <code>function</code> | **Example** + ```javascript // session middleware will run before authorize router @@ -223,9 +229,11 @@ router.use(['/users', '/admin'], userAuth()); app.use(router.routes()); ``` + <a name="module_egg-router--Router+prefix"></a> #### router.prefix(prefix) ⇒ <code>Router</code> + Set the path prefix for a Router instance that was already initialized. **Kind**: instance method of <code>[Router](#exp_module_egg-router--Router)</code> @@ -235,12 +243,15 @@ Set the path prefix for a Router instance that was already initialized. | prefix | <code>String</code> | **Example** + ```javascript router.prefix('/things/:thing_id') ``` + <a name="module_egg-router--Router+allowedMethods"></a> #### router.allowedMethods([options]) ⇒ <code>function</code> + Returns separate middleware for responding to `OPTIONS` requests with an `Allow` header containing the allowed methods, as well as responding with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. @@ -255,6 +266,7 @@ with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. | [options.methodNotAllowed] | <code>function</code> | throw the returned value in place of the default MethodNotAllowed error | **Example** + ```javascript var Koa = require('koa'); var Router = require('egg-router'); @@ -283,9 +295,11 @@ app.use(router.allowedMethods({ methodNotAllowed: () => new Boom.methodNotAllowed() })); ``` + <a name="module_egg-router--Router+redirect"></a> #### router.redirect(source, destination, [code]) ⇒ <code>Router</code> + Redirect `source` to `destination` URL with optional 30x status `code`. Both `source` and `destination` can be route names. @@ -314,6 +328,7 @@ router.all('/login', ctx => { <a name="module_egg-router--Router+route"></a> #### router.route(name) ⇒ <code>Layer</code> | <code>false</code> + Lookup route with given `name`. **Kind**: instance method of <code>[Router](#exp_module_egg-router--Router)</code> @@ -325,6 +340,7 @@ Lookup route with given `name`. <a name="module_egg-router--Router+url"></a> #### router.url(name, params, [options]) ⇒ <code>String</code> | <code>Error</code> + Generate URL for route. Takes a route name and map of named `params`. **Kind**: instance method of <code>[Router](#exp_module_egg-router--Router)</code> @@ -337,6 +353,7 @@ Generate URL for route. Takes a route name and map of named `params`. | [options.query] | <code>Object</code> | <code>String</code> | query options | **Example** + ```javascript router.get('user', '/users/:id', (ctx, next) => { // ... @@ -359,9 +376,11 @@ router.url('user', { id: 3 }, { query: { limit: 1 } }); router.url('user', { id: 3 }, { query: "limit=1" }); // => "/users/3?limit=1" ``` + <a name="module_egg-router--Router+param"></a> #### router.param(param, middleware) ⇒ <code>Router</code> + Run middleware for named route parameters. Useful for auto-loading or validation. @@ -373,6 +392,7 @@ validation. | middleware | <code>function</code> | **Example** + ```javascript router .param('user', (id, ctx, next) => { @@ -391,9 +411,11 @@ router // /users/3 => {"id": 3, "name": "Alex"} // /users/3/friends => [{"id": 4, "name": "TJ"}] ``` + <a name="module_egg-router--Router.url"></a> #### Router.url(path, params [, options]) ⇒ <code>String</code> + Generate URL from url pattern and given `params`. **Kind**: static method of <code>[Router](#exp_module_egg-router--Router)</code> @@ -406,6 +428,7 @@ Generate URL from url pattern and given `params`. | [options.query] | <code>Object</code> | <code>String</code> | query options | **Example** + ```javascript var url = Router.url('/users/:id', {id: 1}); // => "/users/1" @@ -418,6 +441,11 @@ const url = Router.url('/users/:id', {id: 1}, {query: { active: true }}); Run tests using `npm test`. +## Breaking changes on v3 + +- Drop generator function support +- Drop Node.js < 18.7.0 support + ## License [MIT](LICENSE) diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index f28c546..0000000 --- a/lib/utils.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const convert = require('koa-convert'); -const is = require('is-type-of'); -const co = require('co'); - -module.exports = { - async callFn(fn, args, ctx) { - args = args || []; - if (!is.function(fn)) return; - if (is.generatorFunction(fn)) fn = co.wrap(fn); - return ctx ? fn.call(ctx, ...args) : fn(...args); - }, - - middleware(fn) { - return is.generatorFunction(fn) ? convert(fn) : fn; - }, -}; diff --git a/package.json b/package.json index 9ff3c12..19b3977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,12 @@ { "name": "@eggjs/router", "version": "2.0.1", + "engines": { + "node": ">= 18.7.0" + }, + "publishConfig": { + "access": "public" + }, "description": "Router middleware for egg/koa. Provides RESTful resource routing.", "repository": { "type": "git", @@ -9,10 +15,6 @@ "bugs": { "url": "https://github.com/eggjs/egg/issues" }, - "files": [ - "lib", - "index.js" - ], "author": "eggjs", "keywords": [ "koa", @@ -21,36 +23,62 @@ "route" ], "dependencies": { - "co": "^4.6.0", - "debug": "^3.1.0", - "http-errors": "^1.3.1", - "inflection": "^1.12.0", - "is-type-of": "^1.2.1", - "koa-compose": "^3.0.0", - "koa-convert": "^1.2.0", - "methods": "^1.0.1", + "http-errors": "^2.0.0", + "inflection": "^3.0.0", + "is-type-of": "^2.1.0", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", "path-to-regexp": "^1.1.1", - "urijs": "^1.19.0", - "utility": "^1.15.0" + "urijs": "^1.19.11", + "utility": "^2.1.0" }, "devDependencies": { - "egg-bin": "^4.10.0", - "eslint": "^5.13.0", - "eslint-config-egg": "^7.1.0", - "expect.js": "^0.3.1", - "koa": "^2.7.0", - "mocha": "^2.0.1", - "should": "^6.0.3", - "supertest": "^1.0.1" + "@eggjs/koa": "^2.18.0", + "@types/mocha": "^10.0.6", + "egg-bin": "6", + "eslint": "8", + "eslint-config-egg": "13", + "git-contributor": "^2.1.5", + "supertest": "^1.0.1", + "tshy": "^1.15.1", + "tshy-after": "^1.0.0", + "typescript": "^5.4.5" }, "scripts": { + "lint": "eslint src test --ext ts", + "pretest": "npm run lint -- --fix && npm run prepublishOnly", + "test": "egg-bin test", "test-local": "egg-bin test", - "test": "npm run lint && egg-bin test", - "ci": "npm run lint && egg-bin cov", - "lint": "eslint ." + "preci": "npm run lint && npm run prepublishOnly", + "ci": "egg-bin cov", + "contributor": "git-contributor", + "prepublishOnly": "tshy && tshy-after" }, - "engines": { - "node": ">= 8.5.0" + "license": "MIT", + "type": "module", + "tshy": { + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } }, - "license": "MIT" + "files": [ + "dist", + "src" + ], + "main": "./dist/commonjs/index.js", + "types": "./dist/commonjs/index.d.ts" } diff --git a/lib/egg_router.js b/src/egg_router.js similarity index 100% rename from lib/egg_router.js rename to src/egg_router.js diff --git a/index.js b/src/index.js similarity index 100% rename from index.js rename to src/index.js diff --git a/lib/layer.js b/src/layer.ts similarity index 59% rename from lib/layer.js rename to src/layer.ts index 90cf992..fac14ad 100644 --- a/lib/layer.js +++ b/src/layer.ts @@ -1,49 +1,76 @@ -'use strict'; +import { debuglog } from 'node:util'; +import pathToRegExp, { type Key } from 'path-to-regexp'; +import URI from 'urijs'; +import { decodeURIComponent as safeDecodeURIComponent } from 'utility'; +import type { + MiddlewareFunc, + MiddlewareFuncWithParamProperty, + ParamMiddlewareFunc, +} from './types.js'; + +const debug = debuglog('egg-router:layer'); + +export interface LayerOptions { + prefix?: string; + /** route name */ + name?: string; + /** case sensitive (default: false) */ + sensitive?: boolean; + /** require the trailing slash (default: false) */ + strict?: boolean; + ignoreCaptures?: boolean; +} + +export interface LayerURLOptions { + query?: string | object; +} + +export class Layer { + readonly opts: LayerOptions; + readonly name?: string; + readonly methods: string[] = []; + readonly stack: MiddlewareFuncWithParamProperty[]; + path: string; + regexp: RegExp; + paramNames: Key[] = []; -const debug = require('debug')('egg-router:layer'); -const pathToRegExp = require('path-to-regexp'); -const uri = require('urijs'); -const utility = require('utility'); - -module.exports = class Layer { /** * Initialize a new routing Layer with given `method`, `path`, and `middleware`. * * @param {String|RegExp} path Path string or regular expression. * @param {Array} methods Array of HTTP verbs. - * @param {Array} middleware Layer callback/middleware or series of. + * @param {Array|Function} middlewares Layer callback/middleware or series of. * @param {Object=} opts optional params * @param {String=} opts.name route name * @param {String=} opts.sensitive case sensitive (default: false) * @param {String=} opts.strict require the trailing slash (default: false) * @private */ - constructor(path, methods, middleware, opts) { - this.opts = opts || {}; - this.name = this.opts.name || null; - this.methods = []; - this.paramNames = []; - this.stack = Array.isArray(middleware) ? middleware : [ middleware ]; - - methods.forEach(function(method) { + constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[], opts?: LayerOptions) { + this.opts = opts ?? {}; + this.opts.prefix = this.opts.prefix ?? ''; + this.name = this.opts.name; + this.stack = Array.isArray(middlewares) ? middlewares : [ middlewares ]; + + for (const method of methods) { const l = this.methods.push(method.toUpperCase()); if (this.methods[l - 1] === 'GET') { this.methods.unshift('HEAD'); } - }, this); + } // ensure middleware is a function - this.stack.forEach(function(fn) { + this.stack.forEach(fn => { const type = (typeof fn); if (type !== 'function') { - throw new Error( + throw new TypeError( methods.toString() + ' `' + (this.opts.name || path) + '`: `middleware` ' - + 'must be a function, not `' + type + '`' + + 'must be a function, not `' + type + '`', ); } - }, this); + }); - this.path = path; + this.path = typeof path === 'string' ? path : String(path); this.regexp = pathToRegExp(path, this.paramNames, this.opts); debug('defined route %s %s', this.methods, this.opts.prefix + this.path); @@ -56,7 +83,7 @@ module.exports = class Layer { * @return {Boolean} matched or not * @private */ - match(path) { + match(path: string): boolean { return this.regexp.test(path); } @@ -69,13 +96,14 @@ module.exports = class Layer { * @return {Object} params object * @private */ - params(path, captures, existingParams) { + params(path: string, captures: Array<string>, existingParams?: Record<string, any>): object { const params = existingParams || {}; for (let len = captures.length, i = 0; i < len; i++) { - if (this.paramNames[i]) { + const paramName = this.paramNames[i]; + if (paramName) { const c = captures[i]; - params[this.paramNames[i].name] = c ? utility.decodeURIComponent(c) : c; + params[paramName.name] = c ? safeDecodeURIComponent(c) : c; } } return params; @@ -88,9 +116,10 @@ module.exports = class Layer { * @return {Array.<String>} captures strings * @private */ - captures(path) { + captures(path: string): Array<string> { if (this.opts.ignoreCaptures) return []; - return path.match(this.regexp).slice(1); + const m = path.match(this.regexp); + return m ? m.slice(1) : []; } /** @@ -109,43 +138,53 @@ module.exports = class Layer { * @return {String} url string * @private */ - url(params, options) { - let args = params; + url(params: object, options?: LayerURLOptions): string { + let args: Array<string | number> | object = params; const url = this.path.replace(/\(\.\*\)/g, ''); const toPath = pathToRegExp.compile(url); if (typeof params !== 'object') { + // route.url(123, 456, options); args = Array.prototype.slice.call(arguments); - if (typeof args[args.length - 1] === 'object') { - options = args[args.length - 1]; - args = args.slice(0, args.length - 1); + if (Array.isArray(args)) { + if (typeof args[args.length - 1] === 'object') { + options = args[args.length - 1]; + args = args.slice(0, args.length - 1); + } } } const tokens = pathToRegExp.parse(url); - let replace = {}; + let replace: Record<string, any> = {}; - if (args instanceof Array) { + if (Array.isArray(args)) { for (let len = tokens.length, i = 0, j = 0; i < len; i++) { - if (tokens[i].name) replace[tokens[i].name] = args[j++]; + const token = tokens[i]; + if (typeof token === 'object' && token.name) { + replace[token.name] = args[j++]; + } } - } else if (tokens.some(token => token.name)) { + } else if (tokens.some(token => typeof token === 'object' && token.name)) { replace = params; } else { options = params; } - let replaced = toPath(replace); + const replaced = toPath(replace); - if (options && options.query) { - replaced = new uri(replaced); - replaced.search(options.query); - return replaced.toString(); + if (options?.query) { + const urlObject = new URI(replaced); + urlObject.search(options.query); + return urlObject.toString(); } return replaced; } + #urlWithArgs(args: any[], options?: object) { + + } + /** * Run validations on route named parameters. * @@ -168,15 +207,15 @@ module.exports = class Layer { * @return {Layer} layer instance * @private */ - param(param, fn) { + param(param: string, fn: ParamMiddlewareFunc): Layer { const stack = this.stack; const params = this.paramNames; - const middleware = function(ctx, next) { + const middleware: MiddlewareFuncWithParamProperty = function(this: any, ctx, next) { return fn.call(this, ctx.params[param], ctx, next); }; middleware.param = param; - const names = params.map(function(p) { + const names = params.map(p => { return p.name; }); @@ -205,13 +244,12 @@ module.exports = class Layer { * @return {Layer} layer instance * @private */ - setPrefix(prefix) { + setPrefix(prefix: string): Layer { if (this.path) { this.path = prefix + this.path; this.paramNames = []; this.regexp = pathToRegExp(this.path, this.paramNames, this.opts); } - return this; } -}; +} diff --git a/lib/router.js b/src/router.js similarity index 100% rename from lib/router.js rename to src/router.js diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5cb66f7 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,4 @@ +export type Next = () => Promise<void>; +export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void; +export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string }; +export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..b8bd1f2 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,19 @@ +import { isFunction, isGeneratorFunction } from 'is-type-of'; + +export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<unknown> { + args = args || []; + if (!isFunction(fn)) { + return; + } + if (isGeneratorFunction(fn)) { + throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`); + } + return ctx ? fn.call(ctx, ...args) : fn(...args); +} + +export function middleware(fn: Function) { + if (isGeneratorFunction(fn)) { + throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`); + } + return fn; +} diff --git a/test/lib/layer.test.js b/test/lib/layer.test.ts similarity index 96% rename from test/lib/layer.test.js rename to test/lib/layer.test.ts index 536f9ec..c11ebe1 100644 --- a/test/lib/layer.test.js +++ b/test/lib/layer.test.ts @@ -1,15 +1,12 @@ -'use strict'; +import Application from '@eggjs/koa'; +import http from 'http'; +import request from 'supertest'; +import Router from '../../lib/router'; +import Layer from '../../lib/layer'; -const Koa = require('koa'); -const http = require('http'); -const request = require('supertest'); -require('should'); -const Router = require('../../lib/router'); -const Layer = require('../../lib/layer'); - -describe('test/lib/layer.test.js', function() { +describe('test/lib/layer.test.js', () => { it('composes multiple callbacks/middlware', function(done) { - const app = new Koa(); + const app = new Application(); const router = new Router(); app.use(router.routes()); router.get( @@ -21,7 +18,7 @@ describe('test/lib/layer.test.js', function() { function(ctx, next) { ctx.status = 204; return next(); - } + }, ); request(http.createServer(app.callback())) .get('/programming/how-to-node') diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} From 13c6fb5ed579d91952088e95c09dc4294e557a40 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Sat, 8 Jun 2024 23:53:26 +0800 Subject: [PATCH 02/13] f --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 5a2a289..003b101 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,6 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest' - version: '18, 20, 22' + version: '18.7.0, 18, 20, 22' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 351f2fccc73fabfa3822609f3eb46cad5984467b Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:03:59 +0800 Subject: [PATCH 03/13] f --- .gitignore | 3 + package.json | 7 + src/index.js | 10 - src/index.ts | 3 + src/layer.ts | 48 +- src/router.js | 724 ----------------------------- src/router.ts | 1002 ++++++++++++++++++++++++++++++++++++++++ src/types.ts | 1 + src/utils.ts | 7 +- test/lib/layer.test.ts | 231 +++++---- 10 files changed, 1155 insertions(+), 881 deletions(-) delete mode 100644 src/index.js create mode 100644 src/index.ts delete mode 100644 src/router.js create mode 100644 src/router.ts diff --git a/.gitignore b/.gitignore index 6903f3f..3a99497 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ yarn.lock !.env.test .DS_Store + +.tshy* +dist diff --git a/package.json b/package.json index 19b3977..1027993 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,12 @@ }, "devDependencies": { "@eggjs/koa": "^2.18.0", + "@eggjs/tsconfig": "^1.3.3", + "@types/koa-compose": "^3.2.8", + "@types/methods": "^1.1.4", "@types/mocha": "^10.0.6", + "@types/supertest": "^6.0.2", + "@types/urijs": "^1.19.25", "egg-bin": "6", "eslint": "8", "eslint-config-egg": "13", @@ -66,10 +71,12 @@ "./package.json": "./package.json", ".": { "import": { + "source": "./src/index.ts", "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { + "source": "./src/index.ts", "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 7a9f149..0000000 --- a/src/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const KoaRouter = require('./lib/router'); -const EggRouter = require('./lib/egg_router'); - -// for compact -module.exports = KoaRouter; -module.exports.KoaRouter = KoaRouter; -module.exports.EggRouter = EggRouter; - diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..54a0bbe --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './router.js'; +// import EggRouter from './lib/egg_router'; + diff --git a/src/layer.ts b/src/layer.ts index fac14ad..8f936d0 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -19,6 +19,7 @@ export interface LayerOptions { /** require the trailing slash (default: false) */ strict?: boolean; ignoreCaptures?: boolean; + end?: boolean; } export interface LayerURLOptions { @@ -46,7 +47,12 @@ export class Layer { * @param {String=} opts.strict require the trailing slash (default: false) * @private */ - constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[], opts?: LayerOptions) { + constructor(path: string | RegExp, methods: string[], middlewares: MiddlewareFunc | MiddlewareFunc[], + opts?: LayerOptions | string) { + if (typeof opts === 'string') { + // new Layer(path, methods, middlewares, name); + opts = { name: opts }; + } this.opts = opts ?? {}; this.opts.prefix = this.opts.prefix ?? ''; this.name = this.opts.name; @@ -90,14 +96,14 @@ export class Layer { /** * Returns map of URL parameters for given `path` and `paramNames`. * - * @param {String} path path string + * @param {String} _path path string * @param {Array.<String>} captures captures strings - * @param {Object=} existingParams existing params + * @param {Object=} [existingParams] existing params * @return {Object} params object * @private */ - params(path: string, captures: Array<string>, existingParams?: Record<string, any>): object { - const params = existingParams || {}; + params(_path: string, captures: Array<string>, existingParams?: Record<string, string>): Record<string, string> { + const params = existingParams ?? {}; for (let len = captures.length, i = 0; i < len; i++) { const paramName = this.paramNames[i]; @@ -130,26 +136,30 @@ export class Layer { * ```javascript * var route = new Layer(['GET'], '/users/:id', fn); * + * route.url(123); // => "/users/123" + * route.url('123'); // => "/users/123" * route.url({ id: 123 }); // => "/users/123" * ``` * * @param {Object} params url parameters - * @param {Object} [options] optional parameters + * @param {Object} paramsOrOptions optional parameters * @return {String} url string * @private */ - url(params: object, options?: LayerURLOptions): string { - let args: Array<string | number> | object = params; + url(params?: string | number | object, ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string { + let args: Array<string | number | object> | object = params as object; const url = this.path.replace(/\(\.\*\)/g, ''); const toPath = pathToRegExp.compile(url); + let options: LayerURLOptions | undefined; - if (typeof params !== 'object') { - // route.url(123, 456, options); - args = Array.prototype.slice.call(arguments); + if (params !== undefined && typeof params !== 'object') { + args = [ params, ...paramsOrOptions ]; + // route.url(params1, params2, ..., options); if (Array.isArray(args)) { - if (typeof args[args.length - 1] === 'object') { - options = args[args.length - 1]; - args = args.slice(0, args.length - 1); + const lastIndex = args.length - 1; + if (typeof args[lastIndex] === 'object') { + options = paramsOrOptions[lastIndex] as LayerURLOptions; + args = paramsOrOptions.slice(0, lastIndex); } } } @@ -165,9 +175,11 @@ export class Layer { } } } else if (tokens.some(token => typeof token === 'object' && token.name)) { - replace = params; + // route.url(params); + replace = params as object; } else { - options = params; + // route.url(options); + options = params as LayerURLOptions; } const replaced = toPath(replace); @@ -181,10 +193,6 @@ export class Layer { return replaced; } - #urlWithArgs(args: any[], options?: object) { - - } - /** * Run validations on route named parameters. * diff --git a/src/router.js b/src/router.js deleted file mode 100644 index e101a8a..0000000 --- a/src/router.js +++ /dev/null @@ -1,724 +0,0 @@ -'use strict'; - -/** - * RESTful resource routing middleware for eggjs. - */ - -const debug = require('debug')('egg-router'); -const compose = require('koa-compose'); -const HttpError = require('http-errors'); -const methods = require('methods'); -const Layer = require('./layer'); - -/** - * @module koa-router - */ -class Router { - /** - * Create a new router. - * - * @example - * - * Basic usage: - * - * ```javascript - * var Koa = require('koa'); - * var Router = require('koa-router'); - * - * var app = new Koa(); - * var router = new Router(); - * - * router.get('/', (ctx, next) => { - * // ctx.router available - * }); - * - * app - * .use(router.routes()) - * .use(router.allowedMethods()); - * ``` - * - * @alias module:koa-router - * @param {Object=} opts optional - * @param {String=} opts.prefix prefix router paths - * @class - */ - constructor(opts) { - this.opts = opts || {}; - this.methods = this.opts.methods || [ - 'HEAD', - 'OPTIONS', - 'GET', - 'PUT', - 'PATCH', - 'POST', - 'DELETE', - ]; - - this.params = {}; - this.stack = []; - } - - /** - * Use given middleware. - * - * Middleware run in the order they are defined by `.use()`. They are invoked - * sequentially, requests start at the first middleware and work their way - * "down" the middleware stack. - * - * @example - * - * ```javascript - * // session middleware will run before authorize - * router - * .use(session()) - * .use(authorize()); - * - * // use middleware only with given path - * router.use('/users', userAuth()); - * - * // or with an array of paths - * router.use(['/users', '/admin'], userAuth()); - * - * app.use(router.routes()); - * ``` - * - * @param {String=} path path string - * @param {Function} middleware middleware function - * @return {Router} router instance - */ - use(/* path, middleware */) { - const router = this; - const middleware = Array.prototype.slice.call(arguments); - let path; - - // support array of paths - if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { - middleware[0].forEach(function(p) { - router.use.apply(router, [ p ].concat(middleware.slice(1))); - }); - - return this; - } - - const hasPath = typeof middleware[0] === 'string'; - if (hasPath) { - path = middleware.shift(); - } - - middleware.forEach(function(m) { - if (m.router) { - m.router.stack.forEach(function(nestedLayer) { - if (path) nestedLayer.setPrefix(path); - if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix); - router.stack.push(nestedLayer); - }); - - if (router.params) { - Object.keys(router.params).forEach(function(key) { - m.router.param(key, router.params[key]); - }); - } - } else { - router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath }); - } - }); - - return this; - } - - /** - * Set the path prefix for a Router instance that was already initialized. - * - * @example - * - * ```javascript - * router.prefix('/things/:thing_id') - * ``` - * - * @param {String} prefix prefix string - * @return {Router} router instance - */ - prefix(prefix) { - prefix = prefix.replace(/\/$/, ''); - - this.opts.prefix = prefix; - - this.stack.forEach(function(route) { - route.setPrefix(prefix); - }); - - return this; - } - - /** - * Returns router middleware which dispatches a route matching the request. - * - * @return {Function} middleware function - */ - routes() { - const router = this; - - const dispatch = function dispatch(ctx, next) { - debug('%s %s', ctx.method, ctx.path); - - const path = router.opts.routerPath || ctx.routerPath || ctx.path; - const matched = router.match(path, ctx.method); - - if (ctx.matched) { - ctx.matched.push.apply(ctx.matched, matched.path); - } else { - ctx.matched = matched.path; - } - - ctx.router = router; - - if (!matched.route) return next(); - - const matchedLayers = matched.pathAndMethod; - const layerChain = matchedLayers.reduce(function(memo, layer) { - memo.push(function(ctx, next) { - ctx.captures = layer.captures(path, ctx.captures); - ctx.params = layer.params(path, ctx.captures, ctx.params); - // ctx._matchedRouteName & ctx._matchedRoute for compatibility - ctx._matchedRouteName = ctx.routerName = layer.name; - if (!layer.name) ctx._matchedRouteName = undefined; - ctx._matchedRoute = ctx.routerPath = layer.path; - return next(); - }); - return memo.concat(layer.stack); - }, []); - - return compose(layerChain)(ctx, next); - }; - - dispatch.router = this; - - return dispatch; - } - - /** - * Returns separate middleware for responding to `OPTIONS` requests with - * an `Allow` header containing the allowed methods, as well as responding - * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. - * - * @example - * - * ```javascript - * var Koa = require('koa'); - * var Router = require('koa-router'); - * - * var app = new Koa(); - * var router = new Router(); - * - * app.use(router.routes()); - * app.use(router.allowedMethods()); - * ``` - * - * **Example with [Boom](https://github.com/hapijs/boom)** - * - * ```javascript - * var Koa = require('koa'); - * var Router = require('koa-router'); - * var Boom = require('boom'); - * - * var app = new Koa(); - * var router = new Router(); - * - * app.use(router.routes()); - * app.use(router.allowedMethods({ - * throw: true, - * notImplemented: () => new Boom.notImplemented(), - * methodNotAllowed: () => new Boom.methodNotAllowed() - * })); - * ``` - * - * @param {Object=} options optional params - * @param {Boolean=} options.throw throw error instead of setting status and header - * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error - * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error - * @return {Function} middleware function - */ - allowedMethods(options) { - options = options || {}; - const implemented = this.methods; - - return function allowedMethods(ctx, next) { - return next().then(function() { - const allowed = {}; - - if (!ctx.status || ctx.status === 404) { - ctx.matched.forEach(function(route) { - route.methods.forEach(function(method) { - allowed[method] = method; - }); - }); - - const allowedArr = Object.keys(allowed); - - if (!implemented.includes(ctx.method)) { - if (options.throw) { - let notImplementedThrowable; - if (typeof options.notImplemented === 'function') { - notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function - } else { - notImplementedThrowable = new HttpError.NotImplemented(); - } - throw notImplementedThrowable; - } else { - ctx.status = 501; - ctx.set('Allow', allowedArr.join(', ')); - } - } else if (allowedArr.length) { - if (ctx.method === 'OPTIONS') { - ctx.status = 200; - ctx.body = ''; - ctx.set('Allow', allowedArr.join(', ')); - } else if (!allowed[ctx.method]) { - if (options.throw) { - let notAllowedThrowable; - if (typeof options.methodNotAllowed === 'function') { - notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function - } else { - notAllowedThrowable = new HttpError.MethodNotAllowed(); - } - throw notAllowedThrowable; - } else { - ctx.status = 405; - ctx.set('Allow', allowedArr.join(', ')); - } - } - } - } - }); - }; - } - - /** - * Register route with all methods. - * - * @param {String} name Optional. - * @param {String} path path string - * @param {Function=} middleware You may also pass multiple middleware. - * @param {Function} callback callback function - * @return {Router} router instance - * @private - */ - all(name, path/* , middleware */) { - let middleware; - - if (typeof path === 'string') { - middleware = Array.prototype.slice.call(arguments, 2); - } else { - middleware = Array.prototype.slice.call(arguments, 1); - path = name; - name = null; - } - - this.register(path, methods, middleware, { - name, - }); - - return this; - } - - /** - * Redirect `source` to `destination` URL with optional 30x status `code`. - * - * Both `source` and `destination` can be route names. - * - * ```javascript - * router.redirect('/login', 'sign-in'); - * ``` - * - * This is equivalent to: - * - * ```javascript - * router.all('/login', ctx => { - * ctx.redirect('/sign-in'); - * ctx.status = 301; - * }); - * ``` - * - * @param {String} source URL or route name. - * @param {String} destination URL or route name. - * @param {Number=} code HTTP status code (default: 301). - * @return {Router} router instance - */ - redirect(source, destination, code) { - // lookup source route by name - if (source[0] !== '/') { - source = this.url(source); - } - - // lookup destination route by name - if (destination[0] !== '/') { - destination = this.url(destination); - } - - return this.all(source, ctx => { - ctx.redirect(destination); - ctx.status = code || 301; - }); - } - - /** - * Create and register a route. - * - * @param {String} path Path string. - * @param {Array.<String>} methods Array of HTTP verbs. - * @param {Function} middleware Multiple middleware also accepted. - * @param {Object} [opts] optional params - * @return {Layer} layer instance - * @private - */ - register(path, methods, middleware, opts) { - opts = opts || {}; - - const router = this; - const stack = this.stack; - - // support array of paths - if (Array.isArray(path)) { - path.forEach(function(p) { - router.register.call(router, p, methods, middleware, opts); - }); - - return this; - } - - // create route - const route = new Layer(path, methods, middleware, { - end: opts.end === false ? opts.end : true, - name: opts.name, - sensitive: opts.sensitive || this.opts.sensitive || false, - strict: opts.strict || this.opts.strict || false, - prefix: opts.prefix || this.opts.prefix || '', - ignoreCaptures: opts.ignoreCaptures, - }); - - if (this.opts.prefix) { - route.setPrefix(this.opts.prefix); - } - - // add parameter middleware - Object.keys(this.params).forEach(function(param) { - route.param(param, this.params[param]); - }, this); - - stack.push(route); - - return route; - } - - /** - * Lookup route with given `name`. - * - * @param {String} name route name - * @return {Layer|false} layer instance of false - */ - route(name) { - const routes = this.stack; - - for (let len = routes.length, i = 0; i < len; i++) { - if (routes[i].name && routes[i].name === name) { - return routes[i]; - } - } - - return false; - } - - /** - * Generate URL for route. Takes a route name and map of named `params`. - * - * @example - * - * ```javascript - * router.get('user', '/users/:id', (ctx, next) => { - * // ... - * }); - * - * router.url('user', 3); - * // => "/users/3" - * - * router.url('user', { id: 3 }); - * // => "/users/3" - * - * router.use((ctx, next) => { - * // redirect to named route - * ctx.redirect(ctx.router.url('sign-in')); - * }) - * - * router.url('user', { id: 3 }, { query: { limit: 1 } }); - * // => "/users/3?limit=1" - * - * router.url('user', { id: 3 }, { query: "limit=1" }); - * // => "/users/3?limit=1" - * ``` - * - * @param {String} name route name - * @param {Object} params url parameters - * @param {Object} [options] options parameter - * @param {Object|String} [options.query] query options - * @return {String|Error} string or error instance - */ - url(name/* , params */) { - const route = this.route(name); - - if (route) { - const args = Array.prototype.slice.call(arguments, 1); - return route.url.apply(route, args); - } - - return new Error('No route found for name: ' + name); - } - - /** - * Match given `path` and return corresponding routes. - * - * @param {String} path path string - * @param {String} method method name - * @return {Object.<path, pathAndMethod>} returns layers that matched path and - * path and method. - * @private - */ - match(path, method) { - const layers = this.stack; - let layer; - const matched = { - // matched path - path: [], - // matched path and method(including none method) - pathAndMethod: [], - // method matched or not - route: false, - }; - - for (let len = layers.length, i = 0; i < len; i++) { - layer = layers[i]; - - debug('test %s %s', layer.path, layer.regexp); - - if (layer.match(path)) { - matched.path.push(layer); - - if (layer.methods.length === 0 || layer.methods.includes(method)) { - matched.pathAndMethod.push(layer); - if (layer.methods.length) matched.route = true; - } - // if (layer.methods.length === 0) { - // matched.pathAndMethod.push(layer); - // } else if (layer.methods.includes(method)) { - // matched.pathAndMethod.push(layer); - // matched.route = true; - // } - } - } - - return matched; - } - - /** - * Run middleware for named route parameters. Useful for auto-loading or - * validation. - * - * @example - * - * ```javascript - * router - * .param('user', (id, ctx, next) => { - * ctx.user = users[id]; - * if (!ctx.user) return ctx.status = 404; - * return next(); - * }) - * .get('/users/:user', ctx => { - * ctx.body = ctx.user; - * }) - * .get('/users/:user/friends', ctx => { - * return ctx.user.getFriends().then(function(friends) { - * ctx.body = friends; - * }); - * }) - * // /users/3 => {"id": 3, "name": "Alex"} - * // /users/3/friends => [{"id": 4, "name": "TJ"}] - * ``` - * - * @param {String} param param - * @param {Function} middleware route middleware - * @return {Router} instance - */ - param(param, middleware) { - this.params[param] = middleware; - this.stack.forEach(function(route) { - route.param(param, middleware); - }); - return this; - } -} - -/** - * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such - * as `router.get()` or `router.post()`. - * - * Match URL patterns to callback functions or controller actions using `router.verb()`, - * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`. - * - * Additionaly, `router.all()` can be used to match against all methods. - * - * ```javascript - * router - * .get('/', (ctx, next) => { - * ctx.body = 'Hello World!'; - * }) - * .post('/users', (ctx, next) => { - * // ... - * }) - * .put('/users/:id', (ctx, next) => { - * // ... - * }) - * .del('/users/:id', (ctx, next) => { - * // ... - * }) - * .all('/users/:id', (ctx, next) => { - * // ... - * }); - * ``` - * - * When a route is matched, its path is available at `ctx._matchedRoute` and if named, - * the name is available at `ctx._matchedRouteName` - * - * Route paths will be translated to regular expressions using - * [path-to-regexp](https://github.com/pillarjs/path-to-regexp). - * - * Query strings will not be considered when matching requests. - * - * #### Named routes - * - * Routes can optionally have names. This allows generation of URLs and easy - * renaming of URLs during development. - * - * ```javascript - * router.get('user', '/users/:id', (ctx, next) => { - * // ... - * }); - * - * router.url('user', 3); - * // => "/users/3" - * ``` - * - * #### Multiple middleware - * - * Multiple middleware may be given: - * - * ```javascript - * router.get( - * '/users/:id', - * (ctx, next) => { - * return User.findOne(ctx.params.id).then(function(user) { - * ctx.user = user; - * next(); - * }); - * }, - * ctx => { - * console.log(ctx.user); - * // => { id: 17, name: "Alex" } - * } - * ); - * ``` - * - * ### Nested routers - * - * Nesting routers is supported: - * - * ```javascript - * var forums = new Router(); - * var posts = new Router(); - * - * posts.get('/', (ctx, next) => {...}); - * posts.get('/:pid', (ctx, next) => {...}); - * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); - * - * // responds to "/forums/123/posts" and "/forums/123/posts/123" - * app.use(forums.routes()); - * ``` - * - * #### Router prefixes - * - * Route paths can be prefixed at the router level: - * - * ```javascript - * var router = new Router({ - * prefix: '/users' - * }); - * - * router.get('/', ...); // responds to "/users" - * router.get('/:id', ...); // responds to "/users/:id" - * ``` - * - * #### URL parameters - * - * Named route parameters are captured and added to `ctx.params`. - * - * ```javascript - * router.get('/:category/:title', (ctx, next) => { - * console.log(ctx.params); - * // => { category: 'programming', title: 'how-to-node' } - * }); - * ``` - * - * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is - * used to convert paths to regular expressions. - * - * @name get|put|post|patch|delete|del - * @memberof module:koa-router.prototype - * @param {String} path - * @param {Function=} middleware route middleware(s) - * @param {Function} callback route callback - * @returns {Router} - */ - -methods.forEach(function(method) { - Router.prototype[method] = function(name, path /* , middleware */) { - let middleware; - - if (typeof path === 'string' || path instanceof RegExp) { - middleware = Array.prototype.slice.call(arguments, 2); - } else { - middleware = Array.prototype.slice.call(arguments, 1); - path = name; - name = null; - } - - this.register(path, [ method ], middleware, { - name, - }); - - return this; - }; -}); - -// Alias for `router.delete()` because delete is a reserved word -Router.prototype.del = Router.prototype.delete; - -/** - * Generate URL from url pattern and given `params`. - * - * @example - * - * ```javascript - * var url = Router.url('/users/:id', {id: 1}); - * // => "/users/1" - * ``` - * - * @param {String} path url pattern - * @param {Object} params url parameters - * @return {String} url string - */ -Router.url = function(path/* , params */) { - const args = Array.prototype.slice.call(arguments, 1); - return Layer.prototype.url.apply({ path }, args); -}; - -Router.prototype.middleware = Router.prototype.routes; - -module.exports = Router; diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..3115160 --- /dev/null +++ b/src/router.ts @@ -0,0 +1,1002 @@ +/** + * RESTful resource routing middleware for eggjs. + */ + +import { debuglog } from 'node:util'; +import assert from 'node:assert'; +import compose from 'koa-compose'; +import HttpError from 'http-errors'; +import methods from 'methods'; +import { Layer, LayerURLOptions } from './layer.js'; +import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js'; + +const debug = debuglog('egg-router:router'); + +export type RouterMethod = typeof methods[0]; + +export interface RouterOptions { + methods?: string[]; + prefix?: string; + sensitive?: boolean; + strict?: boolean; + routerPath?: string; +} + +export interface RegisterOptions { + name?: string; + prefix?: string; + sensitive?: boolean; + strict?: boolean; + ignoreCaptures?: boolean; + end?: boolean; +} + +export interface AllowedMethodsOptions { + throw?: boolean; + notImplemented?: () => Error; + methodNotAllowed?: () => Error; +} + +export interface MatchedResult { + // matched path + path: Layer[]; + // matched path and method(including none method) + pathAndMethod: Layer[]; + // method matched or not + route: boolean; +} + +export class Router { + readonly opts: RouterOptions; + readonly methods: string[]; + /** Layer stack */ + readonly stack: Layer[] = []; + readonly params: Record<string, ParamMiddlewareFunc> = {}; + + /** + * Create a new router. + * + * @example + * + * Basic usage: + * + * ```javascript + * var Koa = require('koa'); + * var Router = require('koa-router'); + * + * var app = new Koa(); + * var router = new Router(); + * + * router.get('/', (ctx, next) => { + * // ctx.router available + * }); + * + * app + * .use(router.routes()) + * .use(router.allowedMethods()); + * ``` + * + * @alias module:koa-router + * @param {Object=} opts optional + * @param {String=} opts.prefix prefix router paths + * @class + */ + constructor(opts?: RouterOptions) { + this.opts = opts ?? {}; + this.methods = this.opts.methods ?? [ + 'HEAD', + 'OPTIONS', + 'GET', + 'PUT', + 'PATCH', + 'POST', + 'DELETE', + ]; + } + + /** + * Use given middleware. + * + * Middleware run in the order they are defined by `.use()`. They are invoked + * sequentially, requests start at the first middleware and work their way + * "down" the middleware stack. + * + * @example + * + * ```javascript + * // session middleware will run before authorize + * router + * .use(session()) + * .use(authorize()); + * + * // use middleware only with given path + * router.use('/users', userAuth()); + * + * // or with an array of paths + * router.use(['/users', '/admin'], userAuth()); + * + * app.use(router.routes()); + * ``` + * + * @param {String=} path path string + * @param {Function} middleware middleware function + * @return {Router} router instance + */ + use(...middlewares: MiddlewareFunc[]): Router; + use(path: string | string[], ...middlewares: MiddlewareFunc[]): Router; + use(pathOrMiddleware: string | string[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { + // support array of paths + // use(paths, ...middlewares) + if (Array.isArray(pathOrMiddleware) && typeof pathOrMiddleware[0] === 'string') { + for (const path of pathOrMiddleware) { + this.use(path, ...middlewares); + } + return this; + } + + let path = ''; + let hasPath = false; + if (typeof pathOrMiddleware === 'string') { + // use(path, ...middlewares) + path = pathOrMiddleware; + hasPath = true; + } else if (typeof pathOrMiddleware === 'function') { + // use(...middlewares) + middlewares = [ pathOrMiddleware, ...middlewares ]; + } + + for (const m of middlewares as MiddlewareFuncWithRouter<Router>[]) { + if (m.router) { + for (const nestedLayer of m.router.stack) { + if (path) { + nestedLayer.setPrefix(path); + } + if (this.opts.prefix) { + nestedLayer.setPrefix(this.opts.prefix); + } + this.stack.push(nestedLayer); + } + + if (this.params) { + for (const key in this.params) { + m.router.param(key, this.params[key]); + } + } + } else { + this.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath }); + } + } + + return this; + } + + /** + * Set the path prefix for a Router instance that was already initialized. + * + * @example + * + * ```javascript + * router.prefix('/things/:thing_id') + * ``` + * + * @param {String} prefix prefix string + * @return {Router} router instance + */ + prefix(prefix: string): Router { + prefix = prefix.replace(/\/$/, ''); + this.opts.prefix = prefix; + + for (const layer of this.stack) { + layer.setPrefix(prefix); + } + + return this; + } + + /** + * Returns router middleware which dispatches a route matching the request. + * + * @return {Function} middleware function + */ + routes(): MiddlewareFuncWithRouter<Router> { + const dispatch = (ctx: any, next: Next) => { + const routerPath: string = this.opts.routerPath || ctx.routerPath || ctx.path; + const matched = this.match(routerPath, ctx.method); + debug('dispatch: %s %s, routerPath: %s, matched: %s', + ctx.method, ctx.path, routerPath, matched.route); + + if (ctx.matched) { + (ctx.matched as Layer[]).push(...matched.path); + } else { + ctx.matched = matched.path; + } + ctx.router = this; + + if (!matched.route) { + return next(); + } + + const matchedLayers = matched.pathAndMethod; + const layerChain = matchedLayers.reduce<MiddlewareFunc[]>((memo, layer) => { + memo.push((ctx, next) => { + // ctx.captures = layer.captures(routerPath, ctx.captures); + ctx.captures = layer.captures(routerPath); + ctx.params = layer.params(routerPath, ctx.captures, ctx.params); + // ctx._matchedRouteName & ctx._matchedRoute for compatibility + ctx._matchedRouteName = ctx.routerName = layer.name; + if (!layer.name) { + ctx._matchedRouteName = undefined; + } + ctx._matchedRoute = ctx.routerPath = layer.path; + return next(); + }); + return memo.concat(layer.stack); + }, []); + + return compose(layerChain)(ctx, next); + }; + + dispatch.router = this; + return dispatch; + } + + /** + * @alias to routes() + */ + middleware() { + return this.routes(); + } + + /** + * Returns separate middleware for responding to `OPTIONS` requests with + * an `Allow` header containing the allowed methods, as well as responding + * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. + * + * @example + * + * ```javascript + * var Koa = require('koa'); + * var Router = require('koa-router'); + * + * var app = new Koa(); + * var router = new Router(); + * + * app.use(router.routes()); + * app.use(router.allowedMethods()); + * ``` + * + * **Example with [Boom](https://github.com/hapijs/boom)** + * + * ```javascript + * var Koa = require('koa'); + * var Router = require('koa-router'); + * var Boom = require('boom'); + * + * var app = new Koa(); + * var router = new Router(); + * + * app.use(router.routes()); + * app.use(router.allowedMethods({ + * throw: true, + * notImplemented: () => new Boom.notImplemented(), + * methodNotAllowed: () => new Boom.methodNotAllowed() + * })); + * ``` + * + * @param {Object=} options optional params + * @param {Boolean=} options.throw throw error instead of setting status and header + * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error + * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error + * @return {Function} middleware function + */ + allowedMethods(options?: AllowedMethodsOptions): MiddlewareFunc { + const implemented = this.methods; + + return async function allowedMethods(ctx: any, next: Next) { + await next(); + if (ctx.status && ctx.status !== 404) return; + + const allowed: Record<string, string> = {}; + ctx.matched.forEach((route: Router) => { + route.methods.forEach(method => { + allowed[method] = method; + }); + }); + const allowedMethods = Object.keys(allowed); + + if (!implemented.includes(ctx.method)) { + if (options?.throw) { + let notImplementedThrowable: Error; + if (typeof options?.notImplemented === 'function') { + notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function + } else { + notImplementedThrowable = new HttpError.NotImplemented(); + } + throw notImplementedThrowable; + } else { + ctx.status = 501; + ctx.set('Allow', allowedMethods.join(', ')); + } + } else if (allowedMethods.length > 0) { + if (ctx.method === 'OPTIONS') { + ctx.status = 200; + ctx.body = ''; + ctx.set('Allow', allowedMethods.join(', ')); + } else if (!allowed[ctx.method]) { + if (options?.throw) { + let notAllowedThrowable: Error; + if (typeof options?.methodNotAllowed === 'function') { + notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function + } else { + notAllowedThrowable = new HttpError.MethodNotAllowed(); + } + throw notAllowedThrowable; + } else { + ctx.status = 405; + ctx.set('Allow', allowedMethods.join(', ')); + } + } + } + }; + } + + /** + * Redirect `source` to `destination` URL with optional 30x status `code`. + * + * Both `source` and `destination` can be route names. + * + * ```javascript + * router.redirect('/login', 'sign-in'); + * ``` + * + * This is equivalent to: + * + * ```javascript + * router.all('/login', ctx => { + * ctx.redirect('/sign-in'); + * ctx.status = 301; + * }); + * ``` + * + * @param {String} source URL or route name. + * @param {String} destination URL or route name. + * @param {Number=} status HTTP status code (default: 301). + * @return {Router} router instance + */ + redirect(source: string, destination: string, status: number = 301): Router { + // lookup source route by name + if (source[0] !== '/') { + const routeUrl = this.url(source); + if (routeUrl instanceof Error) { + throw routeUrl; + } + source = routeUrl; + } + + // lookup destination route by name + if (destination[0] !== '/') { + const routeUrl = this.url(destination); + if (routeUrl instanceof Error) { + throw routeUrl; + } + destination = routeUrl; + } + + return this.all(source, ctx => { + ctx.redirect(destination); + ctx.status = status; + }); + } + + /** + * Create and register a route. + * + * @param {String|String[]} path Path string. + * @param {String[]} methods Array of HTTP verbs. + * @param {Function|Function[]} middleware Multiple middleware also accepted. + * @param {Object} [opts] optional params + * @private + */ + register(path: string | string[] | RegExp | RegExp[], + methods: string[], + middleware: MiddlewareFunc | MiddlewareFunc[], + opts?: RegisterOptions): Layer | Layer[] { + opts = opts ?? {}; + // support array of paths + if (Array.isArray(path)) { + const routes: Layer[] = []; + for (const p of path) { + const route = this.register(p, methods, middleware, opts) as Layer; + routes.push(route); + } + return routes; + } + + // create route + const route = new Layer(path, methods, middleware, { + end: opts.end === false ? opts.end : true, + name: opts.name, + sensitive: opts.sensitive ?? this.opts.sensitive ?? false, + strict: opts.strict ?? this.opts.strict ?? false, + prefix: opts.prefix ?? this.opts.prefix ?? '', + ignoreCaptures: opts.ignoreCaptures, + }); + + // FIXME: why??? + if (this.opts.prefix) { + route.setPrefix(this.opts.prefix); + } + + // add parameter middleware to the new route layer + for (const param in this.params) { + route.param(param, this.params[param]); + } + + this.stack.push(route); + return route; + } + + /** + * Lookup route with given `name`. + * + * @param {String} name route name + * @return {Layer|false} layer instance of false + */ + route(name: string): Layer | false { + for (const route of this.stack) { + if (route.name === name) { + return route; + } + } + return false; + } + + /** + * Generate URL for route. Takes a route name and map of named `params`. + * + * @example + * + * ```javascript + * router.get('user', '/users/:id', (ctx, next) => { + * // ... + * }); + * + * router.url('user', 3); + * // => "/users/3" + * + * router.url('user', { id: 3 }); + * // => "/users/3" + * + * router.use((ctx, next) => { + * // redirect to named route + * ctx.redirect(ctx.router.url('sign-in')); + * }) + * + * router.url('user', { id: 3 }, { query: { limit: 1 } }); + * // => "/users/3?limit=1" + * + * router.url('user', { id: 3 }, { query: "limit=1" }); + * // => "/users/3?limit=1" + * ``` + * + * @param {String} name route name + * @param {Object} params url parameters + * @param {Object} [options] options parameter + * @param {Object|String} [options.query] query options + * @return {String|Error} string or error instance + */ + url(name: string, params?: string | number | object, + ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string | Error { + const route = this.route(name); + if (route) { + return route.url(params, ...paramsOrOptions); + } + return new Error(`No route found for name: ${name}`); + } + + /** + * Generate URL from url pattern and given `params`. + * + * @example + * + * ```javascript + * var url = Router.url('/users/:id', { id: 1 }); + * // => "/users/1" + * ``` + * + * @param {String} path url pattern + * @param {Object} params url parameters + * @return {String} url string + */ + static url(path: string, params?: string | number | object, + ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string { + return Layer.prototype.url.call({ path }, params, ...paramsOrOptions); + } + + /** + * Match given `path` and return corresponding routes. + * + * @param {String} path path string + * @param {String} method method name + * @return {Object.<path, pathAndMethod>} returns layers that matched path and + * path and method. + * @private + */ + match(path: string, method: string): MatchedResult { + const matched: MatchedResult = { + // matched path + path: [], + // matched path and method(including none method) + pathAndMethod: [], + // method matched or not + route: false, + }; + + for (const layer of this.stack) { + debug('test %s %s', layer.path, layer.regexp); + + if (layer.match(path)) { + matched.path.push(layer); + + if (layer.methods.length === 0 || layer.methods.includes(method)) { + matched.pathAndMethod.push(layer); + if (layer.methods.length > 0) { + matched.route = true; + } + } + // if (layer.methods.length === 0) { + // matched.pathAndMethod.push(layer); + // } else if (layer.methods.includes(method)) { + // matched.pathAndMethod.push(layer); + // matched.route = true; + // } + } + } + + return matched; + } + + /** + * Run middleware for named route parameters. Useful for auto-loading or + * validation. + * + * @example + * + * ```javascript + * router + * .param('user', (id, ctx, next) => { + * ctx.user = users[id]; + * if (!ctx.user) return ctx.status = 404; + * return next(); + * }) + * .get('/users/:user', ctx => { + * ctx.body = ctx.user; + * }) + * .get('/users/:user/friends', ctx => { + * return ctx.user.getFriends().then(function(friends) { + * ctx.body = friends; + * }); + * }) + * // /users/3 => {"id": 3, "name": "Alex"} + * // /users/3/friends => [{"id": 4, "name": "TJ"}] + * ``` + * + * @param {String} param param + * @param {Function} middleware route middleware + * @return {Router} instance + */ + param(param: string, middleware: ParamMiddlewareFunc): Router { + this.params[param] = middleware; + for (const route of this.stack) { + route.param(param, middleware); + } + return this; + } + + /** + * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such + * as `router.get()` or `router.post()`. + * + * Match URL patterns to callback functions or controller actions using `router.verb()`, + * where **verb** is one of the HTTP verbs such as `router.get()` or `router.post()`. + * + * Additionally, `router.all()` can be used to match against all methods. + * + * ```javascript + * router + * .get('/', (ctx, next) => { + * ctx.body = 'Hello World!'; + * }) + * .post('/users', (ctx, next) => { + * // ... + * }) + * .put('/users/:id', (ctx, next) => { + * // ... + * }) + * .del('/users/:id', (ctx, next) => { + * // ... + * }) + * .all('/users/:id', (ctx, next) => { + * // ... + * }); + * ``` + * + * When a route is matched, its path is available at `ctx._matchedRoute` and if named, + * the name is available at `ctx._matchedRouteName` + * + * Route paths will be translated to regular expressions using + * [path-to-regexp](https://github.com/pillarjs/path-to-regexp). + * + * Query strings will not be considered when matching requests. + * + * #### Named routes + * + * Routes can optionally have names. This allows generation of URLs and easy + * renaming of URLs during development. + * + * ```javascript + * router.get('user', '/users/:id', (ctx, next) => { + * // ... + * }); + * + * router.url('user', 3); + * // => "/users/3" + * ``` + * + * #### Multiple middleware + * + * Multiple middleware may be given: + * + * ```javascript + * router.get( + * '/users/:id', + * (ctx, next) => { + * return User.findOne(ctx.params.id).then(function(user) { + * ctx.user = user; + * next(); + * }); + * }, + * ctx => { + * console.log(ctx.user); + * // => { id: 17, name: "Alex" } + * } + * ); + * ``` + * + * ### Nested routers + * + * Nesting routers is supported: + * + * ```javascript + * var forums = new Router(); + * var posts = new Router(); + * + * posts.get('/', (ctx, next) => {...}); + * posts.get('/:pid', (ctx, next) => {...}); + * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods()); + * + * // responds to "/forums/123/posts" and "/forums/123/posts/123" + * app.use(forums.routes()); + * ``` + * + * #### Router prefixes + * + * Route paths can be prefixed at the router level: + * + * ```javascript + * var router = new Router({ + * prefix: '/users' + * }); + * + * router.get('/', ...); // responds to "/users" + * router.get('/:id', ...); // responds to "/users/:id" + * ``` + * + * #### URL parameters + * + * Named route parameters are captured and added to `ctx.params`. + * + * ```javascript + * router.get('/:category/:title', (ctx, next) => { + * console.log(ctx.params); + * // => { category: 'programming', title: 'how-to-node' } + * }); + * ``` + * + * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is + * used to convert paths to regular expressions. + * + * @name get|put|post|patch|delete|del + * @memberof module:koa-router.prototype + * @param {String} method http method + * @param {String} nameOrPath http path + * @param {Function=} pathOrMiddleware route middleware(s) + * @param {Function} middlewares middlewares + * @return {Router} Router instance + */ + verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + const options: RegisterOptions = {}; + let path: string | RegExp; + if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) { + // verb(method, name, path, ...middlewares) + path = pathOrMiddleware; + assert(typeof nameOrPath === 'string', 'route name should be string'); + options.name = nameOrPath; + } else { + // verb(method, path, ...middlewares) + path = nameOrPath; + middlewares = [ pathOrMiddleware, ...middlewares ]; + } + if (typeof method === 'string') { + method = [ method ]; + } + this.register(path, method, middlewares, options); + return this; + } + + /** + * Register route with all methods. + * + * @param {String} name Optional. + * @param {String} path path string + * @param {Function=} middleware You may also pass multiple middleware. + * @return {Router} router instance + * @private + */ + all(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + all(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares); + } + + // "acl", "bind", "checkout", "connect", "copy", "delete", "get", "head", "link", "lock", + // "m-search", "merge", "mkactivity", "mkcalendar", "mkcol", "move", "notify", "options", + // "patch", "post", "propfind", "proppatch", "purge", "put", "rebind", "report", "search", + // "source", "subscribe", "trace", "unbind", "unlink", "unlock", "unsubscribe" + acl(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + acl(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + acl(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('acl', nameOrPath, pathOrMiddleware, ...middlewares); + } + + bind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + bind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + bind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('bind', nameOrPath, pathOrMiddleware, ...middlewares); + } + + checkout(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + checkout(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + checkout(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('checkout', nameOrPath, pathOrMiddleware, ...middlewares); + } + + connect(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + connect(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + connect(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('connect', nameOrPath, pathOrMiddleware, ...middlewares); + } + + copy(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + copy(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + copy(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('copy', nameOrPath, pathOrMiddleware, ...middlewares); + } + + delete(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + delete(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); + } + + /** Alias for `router.delete()` because delete is a reserved word */ + del(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + del(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + del(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); + } + + get(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + get(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares); + } + + head(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + head(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares); + } + + link(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + link(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + link(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('link', nameOrPath, pathOrMiddleware, ...middlewares); + } + + lock(path: string, ...middlewares: MiddlewareFunc[]): Router; + lock(name: string, path: string, ...middlewares: MiddlewareFunc[]): Router; + lock(nameOrPath: string, pathOrMiddleware: string | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { + return this.verb('lock', nameOrPath, pathOrMiddleware, ...middlewares); + } + + ['m-search'](path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + ['m-search'](name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + ['m-search'](nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('m-search', nameOrPath, pathOrMiddleware, ...middlewares); + } + + merge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + merge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + merge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('merge', nameOrPath, pathOrMiddleware, ...middlewares); + } + + mkactivity(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkactivity(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkactivity(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('mkactivity', nameOrPath, pathOrMiddleware, ...middlewares); + } + + mkcalendar(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkcalendar(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkcalendar(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('mkcalendar', nameOrPath, pathOrMiddleware, ...middlewares); + } + + mkcol(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkcol(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + mkcol(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('mkcol', nameOrPath, pathOrMiddleware, ...middlewares); + } + + move(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + move(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + move(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('move', nameOrPath, pathOrMiddleware, ...middlewares); + } + + notify(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + notify(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + notify(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('notify', nameOrPath, pathOrMiddleware, ...middlewares); + } + + options(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + options(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares); + } + + patch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + patch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares); + } + + post(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + post(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares); + } + + propfind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + propfind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + propfind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('propfind', nameOrPath, pathOrMiddleware, ...middlewares); + } + + proppatch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + proppatch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + proppatch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('proppatch', nameOrPath, pathOrMiddleware, ...middlewares); + } + + purge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + purge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + purge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('purge', nameOrPath, pathOrMiddleware, ...middlewares); + } + + put(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + put(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares); + } + + rebind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + rebind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + rebind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('rebind', nameOrPath, pathOrMiddleware, ...middlewares); + } + + report(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + report(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + report(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('report', nameOrPath, pathOrMiddleware, ...middlewares); + } + + search(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + search(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + search(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('search', nameOrPath, pathOrMiddleware, ...middlewares); + } + + source(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + source(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + source(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('source', nameOrPath, pathOrMiddleware, ...middlewares); + } + + subscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + subscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + subscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('subscribe', nameOrPath, pathOrMiddleware, ...middlewares); + } + + trace(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + trace(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + trace(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('trace', nameOrPath, pathOrMiddleware, ...middlewares); + } + + unbind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unbind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unbind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('unbind', nameOrPath, pathOrMiddleware, ...middlewares); + } + + unlink(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unlink(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unlink(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('unlink', nameOrPath, pathOrMiddleware, ...middlewares); + } + + unlock(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unlock(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unlock(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('unlock', nameOrPath, pathOrMiddleware, ...middlewares); + } + + unsubscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unsubscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; + unsubscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('unsubscribe', nameOrPath, pathOrMiddleware, ...middlewares); + } +} diff --git a/src/types.ts b/src/types.ts index 5cb66f7..b0311c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,3 +2,4 @@ export type Next = () => Promise<void>; export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void; export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string }; export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void; +export type MiddlewareFuncWithRouter<T> = MiddlewareFunc & { router: T }; diff --git a/src/utils.ts b/src/utils.ts index b8bd1f2..8348173 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ import { isFunction, isGeneratorFunction } from 'is-type-of'; +import { MiddlewareFunc } from './types.js'; -export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<unknown> { +type Fn = (...args: any[]) => any; + +export async function callFn(fn: Fn, args: any[], ctx: unknown) { args = args || []; if (!isFunction(fn)) { return; @@ -11,7 +14,7 @@ export async function callFn(fn: Function, args: any[], ctx: unknown): Promise<u return ctx ? fn.call(ctx, ...args) : fn(...args); } -export function middleware(fn: Function) { +export function middleware(fn: MiddlewareFunc) { if (isGeneratorFunction(fn)) { throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`); } diff --git a/test/lib/layer.test.ts b/test/lib/layer.test.ts index c11ebe1..e11eb3a 100644 --- a/test/lib/layer.test.ts +++ b/test/lib/layer.test.ts @@ -1,171 +1,162 @@ +import { strict as assert } from 'node:assert'; import Application from '@eggjs/koa'; -import http from 'http'; import request from 'supertest'; -import Router from '../../lib/router'; -import Layer from '../../lib/layer'; +import { Router } from '../../src/router.js'; +import { Layer } from '../../src/layer.js'; describe('test/lib/layer.test.js', () => { - it('composes multiple callbacks/middlware', function(done) { + it('composes multiple callbacks/middleware', async () => { const app = new Application(); const router = new Router(); app.use(router.routes()); router.get( '/:category/:title', - function(ctx, next) { + (ctx, next) => { ctx.status = 500; return next(); }, - function(ctx, next) { + (ctx, next) => { ctx.status = 204; return next(); }, ); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/programming/how-to-node') - .expect(204) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - describe('Layer#match()', function() { - it('captures URL path parameters', function(done) { - const app = new Koa(); + describe('Layer#match()', () => { + it('captures URL path parameters', async () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); - router.get('/:category/:title', function(ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'match'); - ctx.params.should.have.property('title', 'this'); + router.get('/:category/:title', ctx => { + assert(ctx.params); + assert.equal(ctx.params.category, 'match'); + assert.equal(ctx.params.title, 'this'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/match/this') - .expect(204) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('return orginal path parameters when decodeURIComponent throw error', function(done) { - const app = new Koa(); + it('return original path parameters when decodeURIComponent throw error', async () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); - router.get('/:category/:title', function(ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', '100%'); - ctx.params.should.have.property('title', '101%'); + router.get('/:category/:title', ctx => { + assert(ctx.params); + assert.equal(ctx.params.category, '100%'); + assert.equal(ctx.params.title, '101%'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/100%/101%') - .expect(204) - .end(done); + .expect(204); }); - it('populates ctx.captures with regexp captures', function(done) { - const app = new Koa(); + it('populates ctx.captures with regexp captures', async () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); - router.get(/^\/api\/([^\/]+)\/?/i, function(ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.instanceOf(Array); - ctx.captures.should.have.property(0, '1'); + router.get(/^\/api\/([^\/]+)\/?/i, (ctx, next) => { + assert(ctx.captures); + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], '1'); return next(); - }, function(ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.instanceOf(Array); - ctx.captures.should.have.property(0, '1'); + }, ctx => { + assert(ctx.captures); + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], '1'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/api/1') - .expect(204) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('return orginal ctx.captures when decodeURIComponent throw error', function(done) { - const app = new Koa(); + it('return original ctx.captures when decodeURIComponent throw error', async () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); - router.get(/^\/api\/([^\/]+)\/?/i, function(ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, '101%'); + router.get(/^\/api\/([^\/]+)\/?/i, (ctx, next) => { + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], '101%'); return next(); }, function(ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, '101%'); + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], '101%'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/api/101%') - .expect(204) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('populates ctx.captures with regexp captures include undefiend', function(done) { - const app = new Koa(); + it('populates ctx.captures with regexp captures include undefined', async () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); router.get(/^\/api(\/.+)?/i, function(ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, undefined); + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], undefined); return next(); }, function(ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, undefined); + assert(Array.isArray(ctx.captures)); + assert.equal(ctx.captures.length, 1); + assert.equal(ctx.captures[0], undefined); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/api') - .expect(204) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('should throw friendly error message when handle not exists', function() { - const app = new Koa(); + it('should throw friendly error message when handle not exists', () => { + const app = new Application(); const router = new Router(); app.use(router.routes()); - let notexistHandle; - (function() { - router.get('/foo', notexistHandle); - }).should.throw('get `/foo`: `middleware` must be a function, not `undefined`'); + const notExistsHandle = undefined; - (function() { - router.get('foo router', '/foo', notexistHandle); - }).should.throw('get `foo router`: `middleware` must be a function, not `undefined`'); + assert.throws(() => { + router.get('/foo', notExistsHandle as any); + }, (err: TypeError) => { + assert(err instanceof TypeError); + assert.equal(err.name, 'TypeError'); + assert.equal(err.message, 'get `/foo`: `middleware` must be a function, not `undefined`'); + return true; + }); + + assert.throws(() => { + router.get('foo router', '/foo', notExistsHandle as any); + }, (err: any) => { + assert.equal(err.message, 'get `foo router`: `middleware` must be a function, not `undefined`'); + return true; + }); - (function() { - router.post('/foo', function() {}, notexistHandle); - }).should.throw('post `/foo`: `middleware` must be a function, not `undefined`'); + assert.throws(() => { + router.post('/foo', function() {}, notExistsHandle as any); + }, (err: any) => { + assert.equal(err.message, 'post `/foo`: `middleware` must be a function, not `undefined`'); + return true; + }); }); }); - describe('Layer#param()', function() { - it('composes middleware for param fn', function(done) { - const app = new Koa(); + describe('Layer#param()', () => { + it('composes middleware for param fn', async () => { + const app = new Application(); const router = new Router(); const route = new Layer('/users/:user', [ 'GET' ], [ function(ctx) { ctx.body = ctx.user; } ]); - route.param('user', function(id, ctx, next) { + route.param('user', (id, ctx, next) => { ctx.user = { name: 'alex' }; if (!id) { ctx.status = 404; @@ -175,21 +166,16 @@ describe('test/lib/layer.test.js', () => { }); router.stack.push(route); app.use(router.middleware()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/users/3') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + .expect(200); + assert.equal(res.body.name, 'alex'); }); - it('ignores params which are not matched', function(done) { - const app = new Koa(); + it('ignores params which are not matched', async () => { + const app = new Application(); const router = new Router(); - const route = new Layer('/users/:user', [ 'GET' ], [ function(ctx) { + const route = new Layer('/users/:user', [ 'GET' ], [ ctx => { ctx.body = ctx.user; } ]); route.param('user', function(id, ctx, next) { @@ -210,31 +196,26 @@ describe('test/lib/layer.test.js', () => { }); router.stack.push(route); app.use(router.middleware()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/users/3') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + .expect(200); + assert.equal(res.body.name, 'alex'); }); }); - describe('Layer#url()', function() { - it('generates route URL', function() { + describe('Layer#url()', () => { + it('generates route URL', () => { const route = new Layer('/:category/:title', [ 'get' ], [ function() {} ], 'books'); - let url = route.url({ category: 'programming', title: 'how-to-node' }); - url.should.equal('/programming/how-to-node'); - url = route.url('programming', 'how-to-node'); - url.should.equal('/programming/how-to-node'); + const url1 = route.url({ category: 'programming', title: 'how-to-node' }); + assert.equal(url1, '/programming/how-to-node'); + const url2 = route.url('programming', 'how-to-node'); + assert.equal(url2, '/programming/how-to-node'); }); - it('escapes using encodeURIComponent()', function() { - const route = new Layer('/:category/:title', [ 'get' ], [ function() {} ], 'books'); + it('escapes using encodeURIComponent()', () => { + const route = new Layer('/:category/:title', [ 'get' ], [ () => {} ], 'books'); const url = route.url({ category: 'programming', title: 'how to node' }); - url.should.equal('/programming/how%20to%20node'); + assert.equal(url, '/programming/how%20to%20node'); }); }); }); From 8d7113bed1a6993c2762305ac416f2e9eb9a5ab8 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:26:09 +0800 Subject: [PATCH 04/13] f --- bench/{server.js => server.cjs} | 7 +++---- package.json | 8 ++++---- src/{layer.ts => Layer2.ts} | 0 src/{egg_router.js => egg_router.ts} | 14 ++++++-------- src/index.ts | 4 ++++ src/router.ts | 2 +- test/{lib => }/egg_router.test.js | 2 +- test/index.test.js | 12 ------------ test/index.test.ts | 10 ++++++++++ test/{lib => }/layer.test.ts | 6 +++--- test/{lib => }/router.test.js | 0 test/{lib => }/utils.test.js | 0 12 files changed, 32 insertions(+), 33 deletions(-) rename bench/{server.js => server.cjs} (90%) rename src/{layer.ts => Layer2.ts} (100%) rename src/{egg_router.js => egg_router.ts} (97%) rename test/{lib => }/egg_router.test.js (99%) delete mode 100644 test/index.test.js create mode 100644 test/index.test.ts rename test/{lib => }/layer.test.ts (98%) rename test/{lib => }/router.test.js (100%) rename test/{lib => }/utils.test.js (100%) diff --git a/bench/server.js b/bench/server.cjs similarity index 90% rename from bench/server.js rename to bench/server.cjs index 9464242..4d4dbdf 100644 --- a/bench/server.js +++ b/bench/server.cjs @@ -1,7 +1,6 @@ -'use strict'; - -const Koa = require('koa'); -const Router = require('../'); +/* eslint-disable @typescript-eslint/no-var-requires */ +const Koa = require('@eggjs/koa'); +const Router = require('../dist/commonjs'); const app = new Koa(); const router = new Router(); diff --git a/package.json b/package.json index 1027993..149e16b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,10 @@ "prepublishOnly": "tshy && tshy-after" }, "license": "MIT", + "files": [ + "dist", + "src" + ], "type": "module", "tshy": { "exports": { @@ -82,10 +86,6 @@ } } }, - "files": [ - "dist", - "src" - ], "main": "./dist/commonjs/index.js", "types": "./dist/commonjs/index.d.ts" } diff --git a/src/layer.ts b/src/Layer2.ts similarity index 100% rename from src/layer.ts rename to src/Layer2.ts diff --git a/src/egg_router.js b/src/egg_router.ts similarity index 97% rename from src/egg_router.js rename to src/egg_router.ts index fef7c9f..75b83bb 100644 --- a/src/egg_router.js +++ b/src/egg_router.ts @@ -1,11 +1,9 @@ -'use strict'; - -const is = require('is-type-of'); -const Router = require('./router'); -const utility = require('utility'); -const inflection = require('inflection'); -const assert = require('assert'); -const utils = require('./utils'); +import is from 'is-type-of'; +import Router from './router'; +import utility from 'utility'; +import inflection from 'inflection'; +import assert from 'assert'; +import utils from './utils'; const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ]; diff --git a/src/index.ts b/src/index.ts index 54a0bbe..e12fbb6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,7 @@ +import { Router } from './router.js'; + export * from './router.js'; // import EggRouter from './lib/egg_router'; +export const KoaRouter = Router; +export default Router; diff --git a/src/router.ts b/src/router.ts index 3115160..3659d5f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -7,7 +7,7 @@ import assert from 'node:assert'; import compose from 'koa-compose'; import HttpError from 'http-errors'; import methods from 'methods'; -import { Layer, LayerURLOptions } from './layer.js'; +import { Layer, LayerURLOptions } from './Layer2.js'; import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js'; const debug = debuglog('egg-router:router'); diff --git a/test/lib/egg_router.test.js b/test/egg_router.test.js similarity index 99% rename from test/lib/egg_router.test.js rename to test/egg_router.test.js index 381691a..8a729de 100644 --- a/test/lib/egg_router.test.js +++ b/test/egg_router.test.js @@ -1,6 +1,6 @@ 'use strict'; -const EggRouter = require('../../').EggRouter; +const EggRouter = require('../dist/commonjs').EggRouter; const assert = require('assert'); const is = require('is-type-of'); diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index 17cb5ee..0000000 --- a/test/index.test.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const Router = require('..'); - -describe('test/index.test.js', () => { - it('should expose Router', () => { - assert(typeof Router === 'function'); - assert(typeof Router.KoaRouter === 'function'); - assert(typeof Router.EggRouter === 'function'); - }); -}); diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..837e500 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,10 @@ +import { strict as assert } from 'node:assert'; +import Router, { KoaRouter } from '../src/index.js'; + +describe('test/index.test.ts', () => { + it('should expose Router', () => { + assert(typeof Router === 'function'); + assert(typeof KoaRouter === 'function'); + // assert(typeof Router.EggRouter === 'function'); + }); +}); diff --git a/test/lib/layer.test.ts b/test/layer.test.ts similarity index 98% rename from test/lib/layer.test.ts rename to test/layer.test.ts index e11eb3a..926318f 100644 --- a/test/lib/layer.test.ts +++ b/test/layer.test.ts @@ -1,10 +1,10 @@ import { strict as assert } from 'node:assert'; import Application from '@eggjs/koa'; import request from 'supertest'; -import { Router } from '../../src/router.js'; -import { Layer } from '../../src/layer.js'; +import { Router } from '../src/router.js'; +import { Layer } from '../src/Layer2.js'; -describe('test/lib/layer.test.js', () => { +describe('test/layer.test.ts', () => { it('composes multiple callbacks/middleware', async () => { const app = new Application(); const router = new Router(); diff --git a/test/lib/router.test.js b/test/router.test.js similarity index 100% rename from test/lib/router.test.js rename to test/router.test.js diff --git a/test/lib/utils.test.js b/test/utils.test.js similarity index 100% rename from test/lib/utils.test.js rename to test/utils.test.js From 924e4e187c56c5bd3a95187fefadc60817f643a0 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:26:59 +0800 Subject: [PATCH 05/13] f --- src/{Layer2.ts => Layer.ts} | 0 src/router.ts | 2 +- test/{layer.test.ts => Layer2.test.ts} | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{Layer2.ts => Layer.ts} (100%) rename test/{layer.test.ts => Layer2.test.ts} (98%) diff --git a/src/Layer2.ts b/src/Layer.ts similarity index 100% rename from src/Layer2.ts rename to src/Layer.ts diff --git a/src/router.ts b/src/router.ts index 3659d5f..643d13e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -7,7 +7,7 @@ import assert from 'node:assert'; import compose from 'koa-compose'; import HttpError from 'http-errors'; import methods from 'methods'; -import { Layer, LayerURLOptions } from './Layer2.js'; +import { Layer, LayerURLOptions } from './Layer.js'; import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js'; const debug = debuglog('egg-router:router'); diff --git a/test/layer.test.ts b/test/Layer2.test.ts similarity index 98% rename from test/layer.test.ts rename to test/Layer2.test.ts index 926318f..fd12f45 100644 --- a/test/layer.test.ts +++ b/test/Layer2.test.ts @@ -2,9 +2,9 @@ import { strict as assert } from 'node:assert'; import Application from '@eggjs/koa'; import request from 'supertest'; import { Router } from '../src/router.js'; -import { Layer } from '../src/Layer2.js'; +import { Layer } from '../src/Layer.js'; -describe('test/layer.test.ts', () => { +describe('test/Layer.test.ts', () => { it('composes multiple callbacks/middleware', async () => { const app = new Application(); const router = new Router(); From 9e8e9e9b044ddeb4b395e63fcffd95f50839fada Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:27:12 +0800 Subject: [PATCH 06/13] f --- test/{Layer2.test.ts => Layer.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{Layer2.test.ts => Layer.test.ts} (100%) diff --git a/test/Layer2.test.ts b/test/Layer.test.ts similarity index 100% rename from test/Layer2.test.ts rename to test/Layer.test.ts From 1462978d0a331ff70f2def74985f54c23e13e4df Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:28:31 +0800 Subject: [PATCH 07/13] f --- src/{egg_router.ts => EggRouter.ts} | 0 src/{router.ts => Router2.ts} | 0 src/index.ts | 4 ++-- test/Layer.test.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{egg_router.ts => EggRouter.ts} (100%) rename src/{router.ts => Router2.ts} (100%) diff --git a/src/egg_router.ts b/src/EggRouter.ts similarity index 100% rename from src/egg_router.ts rename to src/EggRouter.ts diff --git a/src/router.ts b/src/Router2.ts similarity index 100% rename from src/router.ts rename to src/Router2.ts diff --git a/src/index.ts b/src/index.ts index e12fbb6..2b057df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { Router } from './router.js'; +import { Router } from './Router2.js'; -export * from './router.js'; +export * from './Router2.js'; // import EggRouter from './lib/egg_router'; export const KoaRouter = Router; diff --git a/test/Layer.test.ts b/test/Layer.test.ts index fd12f45..c4931b5 100644 --- a/test/Layer.test.ts +++ b/test/Layer.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import Application from '@eggjs/koa'; import request from 'supertest'; -import { Router } from '../src/router.js'; +import { Router } from '../src/Router2.js'; import { Layer } from '../src/Layer.js'; describe('test/Layer.test.ts', () => { From 44219c48e5da0cc55024a7a113a0b775af50e2ae Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Mon, 10 Jun 2024 21:29:20 +0800 Subject: [PATCH 08/13] f --- src/{Router2.ts => Router.ts} | 0 src/index.ts | 4 ++-- test/{egg_router.test.js => EggRouter.test.ts} | 0 test/Layer.test.ts | 2 +- test/{router.test.js => Router.test.ts} | 0 test/{utils.test.js => utils.test.ts} | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename src/{Router2.ts => Router.ts} (100%) rename test/{egg_router.test.js => EggRouter.test.ts} (100%) rename test/{router.test.js => Router.test.ts} (100%) rename test/{utils.test.js => utils.test.ts} (100%) diff --git a/src/Router2.ts b/src/Router.ts similarity index 100% rename from src/Router2.ts rename to src/Router.ts diff --git a/src/index.ts b/src/index.ts index 2b057df..5e3332e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { Router } from './Router2.js'; +import { Router } from './Router.js'; -export * from './Router2.js'; +export * from './Router.js'; // import EggRouter from './lib/egg_router'; export const KoaRouter = Router; diff --git a/test/egg_router.test.js b/test/EggRouter.test.ts similarity index 100% rename from test/egg_router.test.js rename to test/EggRouter.test.ts diff --git a/test/Layer.test.ts b/test/Layer.test.ts index c4931b5..df20073 100644 --- a/test/Layer.test.ts +++ b/test/Layer.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import Application from '@eggjs/koa'; import request from 'supertest'; -import { Router } from '../src/Router2.js'; +import { Router } from '../src/Router.js'; import { Layer } from '../src/Layer.js'; describe('test/Layer.test.ts', () => { diff --git a/test/router.test.js b/test/Router.test.ts similarity index 100% rename from test/router.test.js rename to test/Router.test.ts diff --git a/test/utils.test.js b/test/utils.test.ts similarity index 100% rename from test/utils.test.js rename to test/utils.test.ts From 54f16c733ff891222696b1239cbc36976461ab4c Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Tue, 11 Jun 2024 14:21:08 +0800 Subject: [PATCH 09/13] f --- src/EggRouter.ts | 266 ++++----- src/Layer.ts | 25 +- src/Router.ts | 308 ++++++----- src/index.ts | 2 +- src/types.ts | 10 + src/utils.ts | 22 - test/EggRouter.test.ts | 83 +-- test/Router.test.ts | 1160 +++++++++++++++------------------------- test/index.test.ts | 5 +- test/utils.test.ts | 61 --- 10 files changed, 835 insertions(+), 1107 deletions(-) delete mode 100644 src/utils.ts delete mode 100644 test/utils.test.ts diff --git a/src/EggRouter.ts b/src/EggRouter.ts index 75b83bb..f5e38b6 100644 --- a/src/EggRouter.ts +++ b/src/EggRouter.ts @@ -1,13 +1,18 @@ -import is from 'is-type-of'; -import Router from './router'; -import utility from 'utility'; +import assert from 'node:assert'; +import { encodeURIComponent as safeEncodeURIComponent } from 'utility'; import inflection from 'inflection'; -import assert from 'assert'; -import utils from './utils'; +import methods from 'methods'; +import { RegisterOptions, Router, RouterMethod, RouterOptions } from './Router.js'; +import { MiddlewareFunc, ResourcesController } from './types.js'; -const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete' ]; +interface RestfulOptions { + suffix?: string; + namePrefix?: string; + method: string | string[]; + member?: true; +} -const REST_MAP = { +const REST_MAP: Record<string, RestfulOptions> = { index: { suffix: '', method: 'GET', @@ -47,51 +52,117 @@ const REST_MAP = { }, }; +interface Application { + controller: Record<string, any>; +} + /** * FIXME: move these patch into @eggjs/router */ -class EggRouter extends Router { +export class EggRouter extends Router { + readonly app: Application; /** * @class * @param {Object} opts - Router options. * @param {Application} app - Application object. */ - constructor(opts, app) { + constructor(opts: RouterOptions, app: Application) { super(opts); this.app = app; - this.patchRouterMethod(); } - patchRouterMethod() { - // patch router methods to support generator function middleware and string controller - METHODS.concat([ 'all' ]).forEach(method => { - this[method] = (...args) => { - const splited = spliteAndResolveRouterParams({ args, app: this.app }); - // format and rebuild params - args = splited.prefix.concat(splited.middlewares); - return super[method](...args); - }; - }); + #formatRouteParams(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController, + middlewares: (MiddlewareFunc | string | ResourcesController)[]) { + const options: RegisterOptions = {}; + let path: string | RegExp; + if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) { + // verb(method, name, path, ...middlewares) + path = pathOrMiddleware; + assert(typeof nameOrPath === 'string', 'route name should be string'); + options.name = nameOrPath; + } else { + // verb(method, path, ...middlewares) + path = nameOrPath; + middlewares = [ pathOrMiddleware, ...middlewares ]; + } + return { + path, + middlewares, + options, + }; } - /** - * Create and register a route. - * @param {String} path - url path - * @param {Array} methods - Array of HTTP verbs - * @param {Array} middlewares - - * @param {Object} opts - - * @return {Route} this - */ - register(path, methods, middlewares, opts) { - // patch register to support generator function middleware and string controller - middlewares = Array.isArray(middlewares) ? middlewares : [ middlewares ]; - middlewares = convertMiddlewares(middlewares, this.app); - path = Array.isArray(path) ? path : [ path ]; - path.forEach(p => super.register(p, methods, middlewares, opts)); + verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middleware: (MiddlewareFunc | string)[]) { + const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware); + if (typeof method === 'string') { + method = [ method ]; + } + this.register(path, method, middlewares, options); return this; } + // const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all' ]; + head(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + head(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares); + } + options(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + options(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares); + } + get(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + get(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares); + } + put(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + put(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares); + } + patch(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + patch(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares); + } + post(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + post(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares); + } + delete(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + delete(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); + } + all(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + all(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; + all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ...middlewares: (MiddlewareFunc | string)[]): Router { + return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares); + } + + register(path: string | string[] | RegExp | RegExp[], + methods: string[], + middleware: MiddlewareFunc | string | (MiddlewareFunc | string | ResourcesController)[], + opts?: RegisterOptions) { + // patch register to support generator function middleware and string controller + middleware = Array.isArray(middleware) ? middleware : [ middleware ]; + const middlewares = convertMiddlewares(middleware, this.app); + return super.register(path, methods, middlewares, opts); + } + /** * restful router api * @param {String} name - Router name @@ -136,43 +207,34 @@ class EggRouter extends Router { * @return {Router} return route object. * @since 1.0.0 */ - resources(...args) { - const splited = spliteAndResolveRouterParams({ args, app: this.app }); - const middlewares = splited.middlewares; + resources(prefix: string, controller: string | ResourcesController): Router; + resources(prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router; + resources(name: string, prefix: string, controller: string | ResourcesController): Router; + resources(name: string, prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router; + resources(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController, + ...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router { + const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware); // last argument is Controller object - const controller = splited.middlewares.pop(); - - let name = ''; - let prefix = ''; - if (splited.prefix.length === 2) { - // router.get('users', '/users') - name = splited.prefix[0]; - prefix = splited.prefix[1]; - } else { - // router.get('/users') - prefix = splited.prefix[0]; - } - + const controller = resolveController(middlewares.pop()!, this.app); for (const key in REST_MAP) { - const action = controller[key]; + const action = controller[key] as MiddlewareFunc; if (!action) continue; const opts = REST_MAP[key]; - let formatedName; + let routeName; if (opts.member) { - formatedName = inflection.singularize(name); + routeName = inflection.singularize(options.name ?? ''); } else { - formatedName = inflection.pluralize(name); + routeName = inflection.pluralize(options.name ?? ''); } if (opts.namePrefix) { - formatedName = opts.namePrefix + formatedName; + routeName = opts.namePrefix + routeName; } - prefix = prefix.replace(/\/$/, ''); - const path = opts.suffix ? `${prefix}/${opts.suffix}` : prefix; + const prefix = (path as string).replace(/\/$/, ''); + const urlPath = opts.suffix ? `${prefix}/${opts.suffix}` : prefix; const method = Array.isArray(opts.method) ? opts.method : [ opts.method ]; - this.register(path, method, middlewares.concat(action), { name: formatedName }); + this.register(urlPath, method, middlewares.concat(action), { name: routeName }); } - return this; } @@ -189,23 +251,23 @@ class EggRouter extends Router { * @return {String} url by path name and query params. * @since 1.0.0 */ - url(name, params) { + url(name: string, params?: Record<string, string | number | (string | number)[]>): string { const route = this.route(name); if (!route) return ''; const args = params; let url = route.path; - assert(!is.regExp(url), `Can't get the url for regExp ${url} for by name '${name}'`); + assert(!(url instanceof RegExp), `Can't get the url for regExp ${url} for by name '${name}'`); const queries = []; if (typeof args === 'object' && args !== null) { - const replacedParams = []; - url = url.replace(/:([a-zA-Z_]\w*)/g, function($0, key) { - if (utility.has(args, key)) { + const replacedParams: string[] = []; + url = url.replace(/:([a-zA-Z_]\w*)/g, ($0, key) => { + if (key in args) { const values = args[key]; replacedParams.push(key); - return utility.encodeURIComponent(Array.isArray(values) ? values[0] : values); + return safeEncodeURIComponent(Array.isArray(values) ? String(values[0]) : String(values)); } return $0; }); @@ -214,15 +276,14 @@ class EggRouter extends Router { if (replacedParams.includes(key)) { continue; } - const values = args[key]; - const encodedKey = utility.encodeURIComponent(key); + const encodedKey = safeEncodeURIComponent(key); if (Array.isArray(values)) { for (const val of values) { - queries.push(`${encodedKey}=${utility.encodeURIComponent(val)}`); + queries.push(`${encodedKey}=${safeEncodeURIComponent(String(val))}`); } } else { - queries.push(`${encodedKey}=${utility.encodeURIComponent(values)}`); + queries.push(`${encodedKey}=${safeEncodeURIComponent(String(values))}`); } } } @@ -239,62 +300,33 @@ class EggRouter extends Router { return url; } - pathFor(name, params) { + /** + * @alias to url() + */ + pathFor(name: string, params?: Record<string, string | number | (string | number)[]>) { return this.url(name, params); } } -/** - * 1. split (name, url, ...middleware, controller) to - * { - * prefix: [name, url] - * middlewares [...middleware, controller] - * } - * - * 2. resolve controller from string to function - * - * @param {Object} options inputs - * @param {Object} options.args router params - * @param {Object} options.app egg application instance - * @return {Object} prefix and middlewares - */ -function spliteAndResolveRouterParams({ args, app }) { - let prefix; - let middlewares; - if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) { - // app.get(name, url, [...middleware], controller) - prefix = args.slice(0, 2); - middlewares = args.slice(2); - } else { - // app.get(url, [...middleware], controller) - prefix = args.slice(0, 1); - middlewares = args.slice(1); - } - // resolve controller - const controller = middlewares.pop(); - middlewares.push(resolveController(controller, app)); - return { prefix, middlewares }; -} - /** * resolve controller from string to function - * @param {String|Function} controller input controller - * @param {Application} app egg application instance - * @return {Function} controller function + * @param {String|Function} controller input controller + * @param {Application} app egg application instance */ -function resolveController(controller, app) { - if (is.string(controller)) { +function resolveController(controller: string | MiddlewareFunc | ResourcesController, app: Application) { + if (typeof controller === 'string') { + // resolveController('foo.bar.Home', app) const actions = controller.split('.'); let obj = app.controller; actions.forEach(key => { obj = obj[key]; - if (!obj) throw new Error(`controller '${controller}' not exists`); + if (!obj) throw new Error(`app.controller.${controller} not exists`); }); - controller = obj; + controller = obj as any; } // ensure controller is exists if (!controller) throw new Error('controller not exists'); - return controller; + return controller as any; } /** @@ -309,17 +341,9 @@ function resolveController(controller, app) { * * @param {Array} middlewares middlewares and controller(last middleware) * @param {Application} app egg application instance - * @return {Array} middlewares */ -function convertMiddlewares(middlewares, app) { +function convertMiddlewares(middlewares: (MiddlewareFunc | string | ResourcesController)[], app: Application) { // ensure controller is resolved - const controller = resolveController(middlewares.pop(), app); - // make middleware support generator function - middlewares = middlewares.map(utils.middleware); - const wrappedController = (ctx, next) => { - return utils.callFn(controller, [ ctx, next ], ctx); - }; - return middlewares.concat([ wrappedController ]); + const controller = resolveController(middlewares.pop()!, app); + return [ ...middlewares as MiddlewareFunc[], controller ]; } - -module.exports = EggRouter; diff --git a/src/Layer.ts b/src/Layer.ts index 8f936d0..2d4b4a1 100644 --- a/src/Layer.ts +++ b/src/Layer.ts @@ -2,6 +2,7 @@ import { debuglog } from 'node:util'; import pathToRegExp, { type Key } from 'path-to-regexp'; import URI from 'urijs'; import { decodeURIComponent as safeDecodeURIComponent } from 'utility'; +import { isGeneratorFunction } from 'is-type-of'; import type { MiddlewareFunc, MiddlewareFuncWithParamProperty, @@ -31,7 +32,7 @@ export class Layer { readonly name?: string; readonly methods: string[] = []; readonly stack: MiddlewareFuncWithParamProperty[]; - path: string; + path: string | RegExp; regexp: RegExp; paramNames: Key[] = []; @@ -67,16 +68,21 @@ export class Layer { // ensure middleware is a function this.stack.forEach(fn => { - const type = (typeof fn); + const type = typeof fn; if (type !== 'function') { throw new TypeError( methods.toString() + ' `' + (this.opts.name || path) + '`: `middleware` ' + 'must be a function, not `' + type + '`', ); } + if (isGeneratorFunction(fn)) { + throw new TypeError( + methods.toString() + ' `' + (this.opts.name || path) + '`: Please use async function instead of generator function', + ); + } }); - this.path = typeof path === 'string' ? path : String(path); + this.path = path; this.regexp = pathToRegExp(path, this.paramNames, this.opts); debug('defined route %s %s', this.methods, this.opts.prefix + this.path); @@ -148,20 +154,25 @@ export class Layer { */ url(params?: string | number | object, ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string { let args: Array<string | number | object> | object = params as object; - const url = this.path.replace(/\(\.\*\)/g, ''); + const url = (this.path as string).replace(/\(\.\*\)/g, ''); const toPath = pathToRegExp.compile(url); let options: LayerURLOptions | undefined; if (params !== undefined && typeof params !== 'object') { args = [ params, ...paramsOrOptions ]; - // route.url(params1, params2, ..., options); + // route.url(stringOrNumber, params1, ..., options); if (Array.isArray(args)) { const lastIndex = args.length - 1; if (typeof args[lastIndex] === 'object') { - options = paramsOrOptions[lastIndex] as LayerURLOptions; - args = paramsOrOptions.slice(0, lastIndex); + options = args[lastIndex]; + args = args.slice(0, lastIndex); } } + } else if (typeof params === 'object') { + if (typeof paramsOrOptions[0] === 'object' && 'query' in paramsOrOptions[0]) { + // route.url(param, options); + options = paramsOrOptions[0]; + } } const tokens = pathToRegExp.parse(url); diff --git a/src/Router.ts b/src/Router.ts index 643d13e..b92527a 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -8,7 +8,7 @@ import compose from 'koa-compose'; import HttpError from 'http-errors'; import methods from 'methods'; import { Layer, LayerURLOptions } from './Layer.js'; -import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc } from './types.js'; +import { MiddlewareFunc, MiddlewareFuncWithRouter, Next, ParamMiddlewareFunc, ResourcesController } from './types.js'; const debug = debuglog('egg-router:router'); @@ -391,13 +391,13 @@ export class Router { /** * Create and register a route. * - * @param {String|String[]} path Path string. + * @param {String|RegExp|(String|RegExp)[]} path Path string. * @param {String[]} methods Array of HTTP verbs. * @param {Function|Function[]} middleware Multiple middleware also accepted. * @param {Object} [opts] optional params * @private */ - register(path: string | string[] | RegExp | RegExp[], + register(path: string | RegExp | (string | RegExp)[], methods: string[], middleware: MiddlewareFunc | MiddlewareFunc[], opts?: RegisterOptions): Layer | Layer[] { @@ -593,6 +593,33 @@ export class Router { return this; } + protected _formatRouteParams(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc | ResourcesController, + middlewares: (MiddlewareFunc | string | ResourcesController)[]) { + const options: RegisterOptions = {}; + let path: string | RegExp | (string | RegExp)[]; + if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) { + // verb(method, name, path, ...middlewares) + path = pathOrMiddleware; + assert(typeof nameOrPath === 'string', 'route name should be string'); + options.name = nameOrPath; + } else if (Array.isArray(pathOrMiddleware)) { + // verb(method, name, paths, ...middlewares) + path = pathOrMiddleware; + assert(typeof nameOrPath === 'string', 'route name should be string'); + options.name = nameOrPath; + } else { + // verb(method, path, ...middlewares) + path = nameOrPath; + middlewares = [ pathOrMiddleware, ...middlewares ]; + } + return { + path, + middlewares, + options, + }; + } + /** * Create `router.verb()` methods, where *verb* is one of the HTTP verbs such * as `router.get()` or `router.post()`. @@ -714,24 +741,15 @@ export class Router { * @param {Function} middlewares middlewares * @return {Router} Router instance */ - verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, - ...middlewares: MiddlewareFunc[]): Router { - const options: RegisterOptions = {}; - let path: string | RegExp; - if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) { - // verb(method, name, path, ...middlewares) - path = pathOrMiddleware; - assert(typeof nameOrPath === 'string', 'route name should be string'); - options.name = nameOrPath; - } else { - // verb(method, path, ...middlewares) - path = nameOrPath; - middlewares = [ pathOrMiddleware, ...middlewares ]; - } + verb(method: string | string[], + nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, + ...middleware: MiddlewareFunc[]): Router { + const { options, path, middlewares } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware); if (typeof method === 'string') { method = [ method ]; } - this.register(path, method, middlewares, options); + this.register(path, method, middlewares as MiddlewareFunc[], options); return this; } @@ -744,9 +762,10 @@ export class Router { * @return {Router} router instance * @private */ - all(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - all(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + all(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + all(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + all(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares); } @@ -755,177 +774,211 @@ export class Router { // "m-search", "merge", "mkactivity", "mkcalendar", "mkcol", "move", "notify", "options", // "patch", "post", "propfind", "proppatch", "purge", "put", "rebind", "report", "search", // "source", "subscribe", "trace", "unbind", "unlink", "unlock", "unsubscribe" - acl(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - acl(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - acl(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + acl(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + acl(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + acl(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('acl', nameOrPath, pathOrMiddleware, ...middlewares); } - bind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - bind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - bind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + bind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + bind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + bind(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('bind', nameOrPath, pathOrMiddleware, ...middlewares); } - checkout(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - checkout(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - checkout(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + checkout(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + checkout(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + checkout(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('checkout', nameOrPath, pathOrMiddleware, ...middlewares); } - connect(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - connect(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - connect(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + connect(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + connect(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + connect(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('connect', nameOrPath, pathOrMiddleware, ...middlewares); } - copy(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - copy(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - copy(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + copy(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + copy(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + copy(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('copy', nameOrPath, pathOrMiddleware, ...middlewares); } - delete(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - delete(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + delete(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + delete(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + delete(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); } /** Alias for `router.delete()` because delete is a reserved word */ - del(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - del(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - del(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + del(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + del(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + del(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); } - get(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - get(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + get(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + get(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + get(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares); } - head(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - head(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + query(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + query(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + query(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { + return this.verb('query', nameOrPath, pathOrMiddleware, ...middlewares); + } + + head(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + head(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + head(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares); } - link(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - link(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - link(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + link(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + link(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + link(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('link', nameOrPath, pathOrMiddleware, ...middlewares); } - lock(path: string, ...middlewares: MiddlewareFunc[]): Router; - lock(name: string, path: string, ...middlewares: MiddlewareFunc[]): Router; - lock(nameOrPath: string, pathOrMiddleware: string | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { + lock(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + lock(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + lock(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, + ...middlewares: MiddlewareFunc[]): Router { return this.verb('lock', nameOrPath, pathOrMiddleware, ...middlewares); } - ['m-search'](path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - ['m-search'](name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - ['m-search'](nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + ['m-search'](path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + ['m-search'](name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + ['m-search'](nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('m-search', nameOrPath, pathOrMiddleware, ...middlewares); } - merge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - merge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - merge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + merge(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + merge(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + merge(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('merge', nameOrPath, pathOrMiddleware, ...middlewares); } - mkactivity(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkactivity(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkactivity(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + mkactivity(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkactivity(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkactivity(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('mkactivity', nameOrPath, pathOrMiddleware, ...middlewares); } - mkcalendar(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkcalendar(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkcalendar(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + mkcalendar(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkcalendar(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkcalendar(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('mkcalendar', nameOrPath, pathOrMiddleware, ...middlewares); } - mkcol(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkcol(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - mkcol(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + mkcol(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkcol(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + mkcol(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('mkcol', nameOrPath, pathOrMiddleware, ...middlewares); } - move(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - move(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - move(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + move(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + move(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + move(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('move', nameOrPath, pathOrMiddleware, ...middlewares); } - notify(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - notify(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - notify(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + notify(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + notify(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + notify(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('notify', nameOrPath, pathOrMiddleware, ...middlewares); } - options(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - options(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + options(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + options(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + options(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares); } - patch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - patch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + patch(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + patch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + patch(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares); } - post(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - post(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + post(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + post(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + post(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares); } - propfind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - propfind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - propfind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + propfind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + propfind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + propfind(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('propfind', nameOrPath, pathOrMiddleware, ...middlewares); } - proppatch(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - proppatch(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - proppatch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + proppatch(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + proppatch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + proppatch(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('proppatch', nameOrPath, pathOrMiddleware, ...middlewares); } - purge(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - purge(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - purge(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + purge(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + purge(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + purge(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('purge', nameOrPath, pathOrMiddleware, ...middlewares); } - put(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - put(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + put(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + put(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + put(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares); } @@ -937,65 +990,74 @@ export class Router { return this.verb('rebind', nameOrPath, pathOrMiddleware, ...middlewares); } - report(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - report(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - report(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + report(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + report(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + report(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('report', nameOrPath, pathOrMiddleware, ...middlewares); } - search(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - search(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - search(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + search(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + search(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + search(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('search', nameOrPath, pathOrMiddleware, ...middlewares); } - source(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - source(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - source(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + source(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + source(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + source(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('source', nameOrPath, pathOrMiddleware, ...middlewares); } - subscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - subscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - subscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + subscribe(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + subscribe(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + subscribe(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('subscribe', nameOrPath, pathOrMiddleware, ...middlewares); } - trace(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - trace(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - trace(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + trace(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + trace(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + trace(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('trace', nameOrPath, pathOrMiddleware, ...middlewares); } - unbind(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unbind(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unbind(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + unbind(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unbind(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unbind(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('unbind', nameOrPath, pathOrMiddleware, ...middlewares); } - unlink(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unlink(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unlink(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + unlink(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unlink(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unlink(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('unlink', nameOrPath, pathOrMiddleware, ...middlewares); } - unlock(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unlock(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unlock(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + unlock(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unlock(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unlock(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('unlock', nameOrPath, pathOrMiddleware, ...middlewares); } - unsubscribe(path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unsubscribe(name: string, path: string | RegExp, ...middlewares: MiddlewareFunc[]): Router; - unsubscribe(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + unsubscribe(path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unsubscribe(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: MiddlewareFunc[]): Router; + unsubscribe(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: MiddlewareFunc[]): Router { return this.verb('unsubscribe', nameOrPath, pathOrMiddleware, ...middlewares); } diff --git a/src/index.ts b/src/index.ts index 5e3332e..37add7a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { Router } from './Router.js'; export * from './Router.js'; -// import EggRouter from './lib/egg_router'; +export * from './EggRouter.js'; export const KoaRouter = Router; export default Router; diff --git a/src/types.ts b/src/types.ts index b0311c0..f606fe7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,3 +3,13 @@ export type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void; export type MiddlewareFuncWithParamProperty = MiddlewareFunc & { param?: string }; export type ParamMiddlewareFunc = (param: string, ctx: any, next: Next) => Promise<void> | void; export type MiddlewareFuncWithRouter<T> = MiddlewareFunc & { router: T }; + +export interface ResourcesController { + index?: MiddlewareFunc; + new?: MiddlewareFunc; + create?: MiddlewareFunc; + show?: MiddlewareFunc; + edit?: MiddlewareFunc; + update?: MiddlewareFunc; + destroy?: MiddlewareFunc; +} diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 8348173..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { isFunction, isGeneratorFunction } from 'is-type-of'; -import { MiddlewareFunc } from './types.js'; - -type Fn = (...args: any[]) => any; - -export async function callFn(fn: Fn, args: any[], ctx: unknown) { - args = args || []; - if (!isFunction(fn)) { - return; - } - if (isGeneratorFunction(fn)) { - throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`); - } - return ctx ? fn.call(ctx, ...args) : fn(...args); -} - -export function middleware(fn: MiddlewareFunc) { - if (isGeneratorFunction(fn)) { - throw new TypeError(`Please use async function instead of generator function: ${fn.toString()}`); - } - return fn; -} diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts index 8a729de..1dec08e 100644 --- a/test/EggRouter.test.ts +++ b/test/EggRouter.test.ts @@ -1,20 +1,18 @@ -'use strict'; +import { strict as assert } from 'node:assert'; +import is from 'is-type-of'; +import { EggRouter } from '../src/index.js'; -const EggRouter = require('../dist/commonjs').EggRouter; -const assert = require('assert'); -const is = require('is-type-of'); - -describe('test/lib/egg_router.test.js', () => { - it('creates new router with egg app', function() { +describe('test/EggRouter.test.ts', () => { + it('creates new router with egg app', () => { const app = { controller: {} }; const router = new EggRouter({}, app); assert(router); [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all', 'resources' ].forEach(method => { - assert(typeof router[method] === 'function'); + assert.equal(typeof Reflect.get(router, method), 'function'); }); }); - it('should app.verb(url, controller) work', () => { + it('should throw error on generator function', () => { const app = { controller: { async foo() { return; }, @@ -24,6 +22,27 @@ describe('test/lib/egg_router.test.js', () => { }, }; + const router = new EggRouter({}, app); + router.get('/foo', app.controller.foo); + assert.throws(() => { + router.post('/hello/world', app.controller.hello.world as any); + }, (err: TypeError) => { + assert(err instanceof TypeError); + assert.equal(err.message, 'post `/hello/world`: Please use async function instead of generator function'); + return true; + }); + }); + + it('should app.verb(url, controller) work', () => { + const app = { + controller: { + async foo() { return; }, + hello: { + world() { return; }, + }, + }, + }; + const router = new EggRouter({}, app); router.get('/foo', app.controller.foo); router.post('/hello/world', app.controller.hello.world); @@ -41,7 +60,7 @@ describe('test/lib/egg_router.test.js', () => { controller: { async foo() { return; }, hello: { - * world() { return; }, + world() { return; }, }, }, }; @@ -65,7 +84,7 @@ describe('test/lib/egg_router.test.js', () => { controller: { async foo() { return; }, hello: { - * world() { return; }, + world() { return; }, }, }, }; @@ -89,7 +108,7 @@ describe('test/lib/egg_router.test.js', () => { controller: { async foo() { return; }, hello: { - * world() { return; }, + world() { return; }, }, }, }; @@ -97,10 +116,10 @@ describe('test/lib/egg_router.test.js', () => { const router = new EggRouter({}, app); assert.throws(() => { router.get('foo', '/foo', 'foobar'); - }, /controller 'foobar' not exists/); + }, /app.controller.foobar not exists/); assert.throws(() => { - router.get('/foo', app.bar); + router.get('/foo', (app as any).bar); }, /controller not exists/); }); @@ -109,18 +128,20 @@ describe('test/lib/egg_router.test.js', () => { controller: { async foo() { return; }, hello: { - * world() { return; }, + world() { return; }, }, }, }; - const generatorMiddleware = function* () { return; }; + const asyncMiddleware1 = async function() { return; }; const asyncMiddleware = async function() { return; }; const commonMiddleware = function() {}; const router = new EggRouter({}, app); - router.get('foo', '/foo', generatorMiddleware, asyncMiddleware, commonMiddleware, 'foo'); - router.post('hello', '/hello/world', generatorMiddleware, asyncMiddleware, commonMiddleware, 'hello.world'); + router.get('foo', '/foo', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'foo'); + router.post('hello', '/hello/world', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'hello.world'); + router.get('foo', '/foo', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'foo'); + router.post('hello', '/hello/world', asyncMiddleware1, asyncMiddleware, commonMiddleware, 'hello.world'); assert(router.stack[0].name === 'foo'); assert(router.stack[0].path === '/foo'); @@ -155,13 +176,13 @@ describe('test/lib/egg_router.test.js', () => { const router = new EggRouter({}, app); router.resources('/post', asyncMiddleware, app.controller.post); - assert(router.stack.length === 5); - assert(router.stack[0].stack.length === 2); + assert.equal(router.stack.length, 5); + assert.equal(router.stack[0].stack.length, 2); router.resources('api_post', '/api/post', app.controller.post); - assert(router.stack.length === 10); - assert(router.stack[5].stack.length === 1); - assert(router.stack[5].name === 'api_posts'); + assert.equal(router.stack.length, 10); + assert.equal(router.stack[5].stack.length, 1); + assert.equal(router.stack[5].name, 'api_posts'); }); it('should router.url work', () => { @@ -169,7 +190,7 @@ describe('test/lib/egg_router.test.js', () => { controller: { async foo() { return; }, hello: { - * world() { return; }, + world() { return; }, }, }, }; @@ -177,13 +198,13 @@ describe('test/lib/egg_router.test.js', () => { router.get('post', '/post/:id', app.controller.foo); router.get('hello', '/hello/world', app.controller.hello.world); - assert(router.url('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar'); - assert(router.url('post', { foo: [ 1, 2 ], bar: 'bar' }) === '/post/:id?foo=1&foo=2&bar=bar'); - assert(router.url('fooo') === ''); - assert(router.url('hello') === '/hello/world'); + assert.equal(router.url('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }), '/post/1?foo=1&foo=2&bar=bar'); + assert.equal(router.url('post', { foo: [ 1, 2 ], bar: 'bar' }), '/post/:id?foo=1&foo=2&bar=bar'); + assert.equal(router.url('fooo'), ''); + assert.equal(router.url('hello'), '/hello/world'); - assert(router.pathFor('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }) === '/post/1?foo=1&foo=2&bar=bar'); - assert(router.pathFor('fooo') === ''); - assert(router.pathFor('hello') === '/hello/world'); + assert.equal(router.pathFor('post', { id: 1, foo: [ 1, 2 ], bar: 'bar' }), '/post/1?foo=1&foo=2&bar=bar'); + assert.equal(router.pathFor('fooo'), ''); + assert.equal(router.pathFor('hello'), '/hello/world'); }); }); diff --git a/test/Router.test.ts b/test/Router.test.ts index ab0bfe8..d32f68d 100644 --- a/test/Router.test.ts +++ b/test/Router.test.ts @@ -1,24 +1,18 @@ -'use strict'; - -const fs = require('fs'); -const http = require('http'); -const Koa = require('koa'); -const methods = require('methods'); -const path = require('path'); -const request = require('supertest'); -const Router = require('../../lib/router'); -const Layer = require('../../lib/layer'); -const expect = require('expect.js'); -const should = require('should'); - -describe('test/lib/router.test.js', function() { - it('creates new router', function(done) { +import { strict as assert } from 'node:assert'; +import fs from 'node:fs'; +import Koa from '@eggjs/koa'; +import methods from 'methods'; +import request from 'supertest'; +import Router from '../src/index.js'; +import { Next } from '../src/types.js'; + +describe('test/lib/router.test.js', () => { + it('creates new router', () => { const router = new Router(); - router.should.be.instanceOf(Router); - done(); + assert(router instanceof Router); }); - it('shares context between routers (gh-205)', function(done) { + it('shares context between routers (gh-205)', async () => { const app = new Koa(); const router1 = new Router(); const router2 = new Router(); @@ -32,17 +26,13 @@ describe('test/lib/router.test.js', function() { return next(); }); app.use(router1.routes()).use(router2.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foo', 'bar'); - done(); - }); + .expect(200); + assert.equal(res.body.foo, 'bar'); }); - it('does not register middleware more than once (gh-184)', function(done) { + it('does not register middleware more than once (gh-184)', async () => { const app = new Koa(); const parentRouter = new Router(); const nestedRouter = new Router(); @@ -51,10 +41,10 @@ describe('test/lib/router.test.js', function() { .get('/first-nested-route', function(ctx) { ctx.body = { n: ctx.n }; }) - .get('/second-nested-route', function(ctx, next) { + .get('/second-nested-route', function(_ctx, next) { return next(); }) - .get('/third-nested-route', function(ctx, next) { + .get('/third-nested-route', function(_ctx, next) { return next(); }); @@ -65,17 +55,13 @@ describe('test/lib/router.test.js', function() { app.use(parentRouter.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/parent-route/first-nested-route') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('n', 1); - done(); - }); + .expect(200); + assert.equal(res.body.n, 1); }); - it('router can be accecced with ctx', function(done) { + it('router can be access with ctx', async () => { const app = new Koa(); const router = new Router(); router.get('home', '/', function(ctx) { @@ -84,17 +70,13 @@ describe('test/lib/router.test.js', function() { }; }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body.url).to.eql('/'); - done(); - }); + .expect(200); + assert.equal(res.body.url, '/'); }); - it('registers multiple middleware for one route', function(done) { + it('registers multiple middleware for one route', async () => { const app = new Koa(); const router = new Router(); @@ -118,54 +100,45 @@ describe('test/lib/router.test.js', function() { app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/double') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body.message).to.eql('Hello World!'); - done(); - }); + .expect(200); + assert.equal(res.body.message, 'Hello World!'); }); - it('does not break when nested-routes use regexp paths', function(done) { + it('does not break when nested-routes use regexp paths', () => { const app = new Koa(); const parentRouter = new Router(); const nestedRouter = new Router(); nestedRouter - .get(/^\/\w$/i, function(ctx, next) { + .get(/^\/\w$/i, function(_ctx, next) { return next(); }) - .get('/first-nested-route', function(ctx, next) { + .get('/first-nested-route', function(_ctx, next) { return next(); }) - .get('/second-nested-route', function(ctx, next) { + .get('/second-nested-route', function(_ctx, next) { return next(); }); - parentRouter.use('/parent-route', function(ctx, next) { + parentRouter.use('/parent-route', function(_ctx, next) { return next(); }, nestedRouter.routes()); app.use(parentRouter.routes()); - app.should.be.ok; - done(); + assert(app); }); - it('exposes middleware factory', function(done) { + it('exposes middleware factory', () => { const router = new Router(); - router.should.have.property('routes'); - router.routes.should.be.type('function'); + assert.equal(typeof router.routes, 'function'); const middleware = router.routes(); - should.exist(middleware); - middleware.should.be.type('function'); - done(); + assert.equal(typeof middleware, 'function'); }); - it('supports promises for async/await', function(done) { + it('supports promises for async/await', async () => { const app = new Koa(); - app.experimental = true; const router = new Router(); router.get('/async', function(ctx) { return new Promise(function(resolve) { @@ -179,17 +152,13 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()).use(router.allowedMethods()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/async') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'promises!'); - done(); - }); + .expect(200); + assert.equal(res.body.msg, 'promises!'); }); - it('matches middleware only if route was matched (gh-182)', function(done) { + it('matches middleware only if route was matched (gh-182)', async () => { const app = new Koa(); const router = new Router(); const otherRouter = new Router(); @@ -205,18 +174,14 @@ describe('test/lib/router.test.js', function() { app.use(router.routes()).use(otherRouter.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/bar') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foo', 'bar'); - expect(res.body).to.not.have.property('bar'); - done(); - }); + .expect(200); + assert.equal(res.body.foo, 'bar'); + assert.equal(res.body.bar, undefined); }); - it('matches first to last', function(done) { + it('matches first to last', async () => { const app = new Koa(); const router = new Router(); @@ -231,17 +196,13 @@ describe('test/lib/router.test.js', function() { ctx.body = { order: 3 }; }); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request(app.use(router.routes()).callback()) .get('/user/account.jsx') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('order', 1); - done(); - }); + .expect(200); + assert.equal(res.body.order, 1); }); - it('does not run subsequent middleware without calling next', function(done) { + it('does not run subsequent middleware without calling next', async () => { const app = new Koa(); const router = new Router(); @@ -252,13 +213,12 @@ describe('test/lib/router.test.js', function() { ctx.body = { order: 1 }; }); - request(http.createServer(app.use(router.routes()).callback())) + await request(app.use(router.routes()).callback()) .get('/user/account.jsx') - .expect(404) - .end(done); + .expect(404); }); - it('nests routers with prefixes at root', function(done) { + it('nests routers with prefixes at root', async () => { const app = new Koa(); const forums = new Router({ prefix: '/forums', @@ -279,35 +239,22 @@ describe('test/lib/router.test.js', function() { forums.use(posts.routes()); - const server = http.createServer(app.use(forums.routes()).callback()); + const server = app.use(forums.routes()).callback(); - request(server) + await request(server) .get('/forums/1/posts') - .expect(204) - .end(function(err) { - if (err) return done(err); - - request(server) - .get('/forums/1') - .expect(404) - .end(function(err) { - if (err) return done(err); - - request(server) - .get('/forums/1/posts/2') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('fid', '1'); - expect(res.body).to.have.property('pid', '2'); - done(); - }); - }); - }); + .expect(204); + await request(server) + .get('/forums/1') + .expect(404); + const res = await request(server) + .get('/forums/1/posts/2') + .expect(200); + assert.equal(res.body.fid, '1'); + assert.equal(res.body.pid, '2'); }); - it('nests routers with prefixes at path', function(done) { + it('nests routers with prefixes at path', async () => { const app = new Koa(); const forums = new Router({ prefix: '/api', @@ -328,35 +275,24 @@ describe('test/lib/router.test.js', function() { forums.use('/forums/:fid', posts.routes()); - const server = http.createServer(app.use(forums.routes()).callback()); + const server = app.use(forums.routes()).callback(); - request(server) + await request(server) .get('/api/forums/1/posts') - .expect(204) - .end(function(err) { - if (err) return done(err); - - request(server) - .get('/api/forums/1') - .expect(404) - .end(function(err) { - if (err) return done(err); - - request(server) - .get('/api/forums/1/posts/2') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('fid', '1'); - expect(res.body).to.have.property('pid', '2'); - done(); - }); - }); - }); + .expect(204); + + await request(server) + .get('/api/forums/1') + .expect(404); + + const res = await request(server) + .get('/api/forums/1/posts/2') + .expect(200); + assert.equal(res.body.fid, '1'); + assert.equal(res.body.pid, '2'); }); - it('runs subrouter middleware after parent', function(done) { + it('runs subrouter middleware after parent', async () => { const app = new Koa(); const subrouter = new Router() .use(function(ctx, next) { @@ -372,17 +308,13 @@ describe('test/lib/router.test.js', function() { return next(); }) .use(subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request(app.use(router.routes()).callback()) .get('/') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'subrouter'); - done(); - }); + .expect(200); + assert.equal(res.body.msg, 'subrouter'); }); - it('runs parent middleware for subrouter routes', function(done) { + it('runs parent middleware for subrouter routes', async () => { const app = new Koa(); const subrouter = new Router() .get('/sub', function(ctx) { @@ -394,59 +326,43 @@ describe('test/lib/router.test.js', function() { return next(); }) .use('/parent', subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request(app.use(router.routes()).callback()) .get('/parent/sub') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'router'); - done(); - }); + .expect(200); + assert.equal(res.body.msg, 'router'); }); - it('matches corresponding requests', function(done) { + it('matches corresponding requests', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get('/:category/:title', function(ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); - ctx.params.should.have.property('title', 'how-to-node'); + assert.equal(ctx.params.category, 'programming'); + assert.equal(ctx.params.title, 'how-to-node'); ctx.status = 204; }); router.post('/:category', function(ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); + assert.equal(ctx.params.category, 'programming'); ctx.status = 204; }); router.put('/:category/not-a-title', function(ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); - ctx.params.should.not.have.property('title'); + assert.equal(ctx.params.category, 'programming'); + assert.equal(ctx.params.title, undefined); ctx.status = 204; }); - const server = http.createServer(app.callback()); - request(server) + const server = app.callback(); + await request(server) .get('/programming/how-to-node') - .expect(204) - .end(function(err) { - if (err) return done(err); - request(server) - .post('/programming') - .expect(204) - .end(function(err) { - if (err) return done(err); - request(server) - .put('/programming/not-a-title') - .expect(204) - .end(function(err) { - done(err); - }); - }); - }); + .expect(204); + await request(server) + .post('/programming') + .expect(204); + await request(server) + .put('/programming/not-a-title') + .expect(204); }); - it('executes route middleware using `app.context`', function(done) { + it('executes route middleware using `app.context`', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); @@ -458,23 +374,19 @@ describe('test/lib/router.test.js', function() { ctx.foo = 'bar'; return next(); }, function(ctx) { - ctx.should.have.property('bar', 'baz'); - ctx.should.have.property('foo', 'bar'); - ctx.should.have.property('app'); - ctx.should.have.property('req'); - ctx.should.have.property('res'); - ctx.status = 204; - done(); + ctx.body = { + bar: ctx.bar, + foo: ctx.foo, + }; }); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/match/this') - .expect(204) - .end(function(err) { - if (err) return done(err); - }); + .expect(200); + assert.equal(res.body.bar, 'baz'); + assert.equal(res.body.foo, 'bar'); }); - it('does not match after ctx.throw()', function(done) { + it('does not match after ctx.throw()', async () => { const app = new Koa(); let counter = 0; const router = new Router(); @@ -486,64 +398,53 @@ describe('test/lib/router.test.js', function() { router.get('/', function() { counter++; }); - const server = http.createServer(app.callback()); - request(server) + await request(app.callback()) .get('/') - .expect(403) - .end(function(err) { - if (err) return done(err); - counter.should.equal(1); - done(); - }); + .expect(403); + assert.equal(counter, 1); }); - it('supports promises for route middleware', function(done) { + it('supports promises for route middleware', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); const readVersion = function() { return new Promise(function(resolve, reject) { - const packagePath = path.join(__dirname, '..', '..', 'package.json'); - fs.readFile(packagePath, 'utf8', function(err, data) { + fs.readFile('package.json', 'utf8', function(err, data) { if (err) return reject(err); resolve(JSON.parse(data).version); }); }); }; router - .get('/', function(ctx, next) { + .get('/', function(_ctx, next) { return next(); }, function(ctx) { return readVersion().then(function() { ctx.status = 204; }); }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/') - .expect(204) - .end(done); + .expect(204); }); - describe('Router#allowedMethods()', function() { - it('responds to OPTIONS requests', function(done) { + describe('Router#allowedMethods()', () => { + it('responds to OPTIONS requests', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(router.allowedMethods()); router.get('/users', function() {}); router.put('/users', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .options('/users') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.header.should.have.property('content-length', '0'); - res.header.should.have.property('allow', 'HEAD, GET, PUT'); - done(); - }); + .expect(200); + assert.equal(res.headers['content-length'], '0'); + assert.equal(res.headers.allow, 'HEAD, GET, PUT'); }); - it('responds with 405 Method Not Allowed', function(done) { + it('responds with 405 Method Not Allowed', async () => { const app = new Koa(); const router = new Router(); router.get('/users', function() {}); @@ -551,17 +452,13 @@ describe('test/lib/router.test.js', function() { router.post('/events', function() {}); app.use(router.routes()); app.use(router.allowedMethods()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .post('/users') - .expect(405) - .end(function(err, res) { - if (err) return done(err); - res.header.should.have.property('allow', 'HEAD, GET, PUT'); - done(); - }); + .expect(405); + assert.equal(res.headers.allow, 'HEAD, GET, PUT'); }); - it('responds ignore allowedMethods when status is already set', function(done) { + it('responds ignore allowedMethods when status is already set', async () => { const app = new Koa(); const router = new Router(); router.get('/users', function() {}); @@ -573,24 +470,21 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); app.use(router.allowedMethods()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .post('/users') - .expect(200) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(200); + assert.equal(res.headers.allow, undefined); }); - it('responds with 405 Method Not Allowed using the "throw" option', function(done) { + it('responds with 405 Method Not Allowed using the "throw" option', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(function(ctx, next) { return next().catch(function(err) { // assert that the correct HTTPError was thrown - err.name.should.equal('MethodNotAllowedError'); - err.statusCode.should.equal(405); + // err.name.should.equal('MethodNotAllowedError'); + // err.statusCode.should.equal(405); // translate the HTTPError to a normal response ctx.body = err.name; @@ -601,26 +495,22 @@ describe('test/lib/router.test.js', function() { router.get('/users', function() {}); router.put('/users', function() {}); router.post('/events', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .post('/users') - .expect(405) - .end(function(err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - done(); - }); + .expect(405); + // the 'Allow' header is not set when throwing + assert.equal(res.headers.allow, undefined); }); - it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', function(done) { + it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(function(ctx, next) { return next().catch(function(err) { // assert that the correct HTTPError was thrown - err.message.should.equal('Custom Not Allowed Error'); - err.statusCode.should.equal(405); + // err.message.should.equal('Custom Not Allowed Error'); + // err.statusCode.should.equal(405); // translate the HTTPError to a normal response ctx.body = err.body; @@ -630,7 +520,7 @@ describe('test/lib/router.test.js', function() { app.use(router.allowedMethods({ throw: true, methodNotAllowed() { - const notAllowedErr = new Error('Custom Not Allowed Error'); + const notAllowedErr: any = new Error('Custom Not Allowed Error'); notAllowedErr.type = 'custom'; notAllowedErr.statusCode = 405; notAllowedErr.body = { @@ -644,46 +534,39 @@ describe('test/lib/router.test.js', function() { router.get('/users', function() {}); router.put('/users', function() {}); router.post('/events', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .post('/users') - .expect(405) - .end(function(err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - res.body.should.eql({ error: 'Custom Not Allowed Error', - statusCode: 405, - otherStuff: true, - }); - done(); - }); + .expect(405); + // the 'Allow' header is not set when throwing + assert.equal(res.headers.allow, undefined); + assert.deepEqual(res.body, { + error: 'Custom Not Allowed Error', + statusCode: 405, + otherStuff: true, + }); }); - it('responds with 501 Not Implemented', function(done) { + it('responds with 501 Not Implemented', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(router.allowedMethods()); router.get('/users', function() {}); router.put('/users', function() {}); - request(http.createServer(app.callback())) + await request(app.callback()) .search('/users') - .expect(501) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(501); }); - it('responds with 501 Not Implemented using the "throw" option', function(done) { + it('responds with 501 Not Implemented using the "throw" option', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(function(ctx, next) { return next().catch(function(err) { // assert that the correct HTTPError was thrown - err.name.should.equal('NotImplementedError'); - err.statusCode.should.equal(501); + // err.name.should.equal('NotImplementedError'); + // err.statusCode.should.equal(501); // translate the HTTPError to a normal response ctx.body = err.name; @@ -693,27 +576,23 @@ describe('test/lib/router.test.js', function() { app.use(router.allowedMethods({ throw: true })); router.get('/users', function() {}); router.put('/users', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .search('/users') - .expect(501) - .end(function(err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - done(); - }); + .expect(501); + // the 'Allow' header is not set when throwing + assert.equal(res.headers.allow, undefined); }); - it('responds with user-provided throwable using the "throw" and "notImplemented" options', function(done) { + it('responds with user-provided throwable using the "throw" and "notImplemented" options', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); app.use(function(ctx, next) { return next().catch(function(err) { // assert that our custom error was thrown - err.message.should.equal('Custom Not Implemented Error'); - err.type.should.equal('custom'); - err.statusCode.should.equal(501); + // err.message.should.equal('Custom Not Implemented Error'); + // err.type.should.equal('custom'); + // err.statusCode.should.equal(501); // translate the HTTPError to a normal response ctx.body = err.body; @@ -723,7 +602,7 @@ describe('test/lib/router.test.js', function() { app.use(router.allowedMethods({ throw: true, notImplemented() { - const notImplementedErr = new Error('Custom Not Implemented Error'); + const notImplementedErr: any = new Error('Custom Not Implemented Error'); notImplementedErr.type = 'custom'; notImplementedErr.statusCode = 501; notImplementedErr.body = { @@ -736,22 +615,19 @@ describe('test/lib/router.test.js', function() { })); router.get('/users', function() {}); router.put('/users', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .search('/users') - .expect(501) - .end(function(err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - res.body.should.eql({ error: 'Custom Not Implemented Error', - statusCode: 501, - otherStuff: true, - }); - done(); - }); + .expect(501); + // the 'Allow' header is not set when throwing + assert.equal(res.header.allow, undefined); + assert.deepEqual(res.body, { + error: 'Custom Not Implemented Error', + statusCode: 501, + otherStuff: true, + }); }); - it('does not send 405 if route matched but status is 404', function(done) { + it('does not send 405 if route matched but status is 404', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); @@ -759,16 +635,12 @@ describe('test/lib/router.test.js', function() { router.get('/users', function(ctx) { ctx.status = 404; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/users') - .expect(404) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(404); }); - it('sets the allowed methods to a single Allow header #273', function(done) { + it('sets the allowed methods to a single Allow header #273', async () => { // https://tools.ietf.org/html/rfc7231#section-7.4.1 const app = new Koa(); const router = new Router(); @@ -777,21 +649,14 @@ describe('test/lib/router.test.js', function() { router.get('/', function() {}); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .options('/') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.header.should.have.property('allow', 'HEAD, GET'); - const allowHeaders = res.res.rawHeaders.filter(item => item === 'Allow'); - expect(allowHeaders.length).to.eql(1); - done(); - }); + .expect(200); + assert.equal(res.header.allow, 'HEAD, GET'); }); - }); - it('supports custom routing detect path: ctx.routerPath', function(done) { + it('supports custom routing detect path: ctx.routerPath', async () => { const app = new Koa(); const router = new Router(); app.use(function(ctx, next) { @@ -805,65 +670,66 @@ describe('test/lib/router.test.js', function() { ctx.body = ctx.method + ' ' + ctx.url; }); - request(http.createServer(app.callback())) + await request(app.callback()) .get('/users') .set('Host', 'helloworld.example.com') .expect(200) - .expect('GET /users', done); + .expect('GET /users'); }); - describe('Router#[verb]()', function() { - it('registers route specific to HTTP verb', function() { + describe('Router#[verb]()', () => { + it('registers route specific to HTTP verb', () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); methods.forEach(function(method) { - router.should.have.property(method); - router[method].should.be.type('function'); - router[method]('/', function() {}); + assert(method in router); + assert(typeof Reflect.get(router, method) === 'function'); + Reflect.get(router, method).call(router, '/', function() {}); }); - router.stack.should.have.length(methods.length); + assert.equal(router.stack.length, methods.length); }); - it('registers route with a regexp path', function() { + it('registers route with a regexp path', () => { const router = new Router(); methods.forEach(function(method) { - router[method](/^\/\w$/i, function() {}).should.equal(router); + assert.equal(Reflect.get(router, method).call(router, /^\/\w$/i, function() {}), router); }); }); - it('registers route with a given name', function() { + it('registers route with a given name', () => { const router = new Router(); methods.forEach(function(method) { - router[method](method, '/', function() {}).should.equal(router); + assert.equal(Reflect.get(router, method).call(router, '/', function() {}), router); }); }); - it('registers route with with a given name and regexp path', function() { + it('registers route with with a given name and regexp path', () => { const router = new Router(); methods.forEach(function(method) { - router[method](method, /^\/$/i, function() {}).should.equal(router); + assert.equal(Reflect.get(router, method).call(router, /^\/$/i, function() {}), router); }); }); - it('enables route chaining', function() { + it('enables route chaining', () => { const router = new Router(); methods.forEach(function(method) { - router[method]('/', function() {}).should.equal(router); + assert(Reflect.get(router, method.toLowerCase()), `${method.toLowerCase()} not exists`); + assert.equal(Reflect.get(router, method.toLowerCase()).call(router, '/', function() {}), router); }); }); - it('registers array of paths (gh-203)', function() { + it('registers array of paths (gh-203)', () => { const router = new Router(); - router.get([ '/one', '/two' ], function(ctx, next) { + router.get([ '/one', '/two' ], function(_ctx, next) { return next(); }); - expect(router.stack).to.have.property('length', 2); - expect(router.stack[0]).to.have.property('path', '/one'); - expect(router.stack[1]).to.have.property('path', '/two'); + assert.equal(router.stack.length, 2); + assert.equal(router.stack[0].path, '/one'); + assert.equal(router.stack[1].path, '/two'); }); - it('resolves non-parameterized routes without attached parameters', function(done) { + it('resolves non-parameterized routes without attached parameters', async () => { const app = new Koa(); const router = new Router(); @@ -884,23 +750,17 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/notparameter') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body.param).to.equal(undefined); - expect(res.body.routerName).to.equal(null); - expect(res.body.routerPath).to.equal('/notparameter'); - done(); - }); + .expect(200); + assert.equal(res.body.param, undefined); + assert.equal(res.body.routerName, undefined); + assert.equal(res.body.routerPath, '/notparameter'); }); - }); - describe('Router#use()', function() { - it('uses router middleware without path', function(done) { + describe('Router#use()', () => { + it('uses router middleware without path', async () => { const app = new Koa(); const router = new Router(); @@ -921,18 +781,13 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/foo/bar') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); + .expect(200); + assert.equal(res.body.foobar, 'foobar'); }); - it('uses router middleware at given path', function(done) { + it('uses router middleware at given path', async () => { const app = new Koa(); const router = new Router(); @@ -948,18 +803,13 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/foo/bar') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); + .expect(200); + assert.equal(res.body.foobar, 'foobar'); }); - it('runs router middleware before subrouter middleware', function(done) { + it('runs router middleware before subrouter middleware', async () => { const app = new Koa(); const router = new Router(); const subrouter = new Router(); @@ -982,18 +832,13 @@ describe('test/lib/router.test.js', function() { router.use('/foo', subrouter.routes()); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/foo/bar') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); + .expect(200); + assert.equal(res.body.foobar, 'foobar'); }); - it('assigns middleware to array of paths', function(done) { + it('assigns middleware to array of paths', async () => { const app = new Koa(); const router = new Router(); @@ -1016,28 +861,21 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); - request(http.createServer(app.callback())) + let res = await request(app.callback()) .get('/foo') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foobar', 'foobar'); - request(http.createServer(app.callback())) - .get('/bar') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); - }); + .expect(200); + assert.equal(res.body.foobar, 'foobar'); + res = await request(app.callback()) + .get('/bar') + .expect(200); + assert.equal(res.body.foobar, 'foobar'); }); - it('without path, does not set params.0 to the matched path - gh-247', function(done) { + it('without path, does not set params.0 to the matched path - gh-247', async () => { const app = new Koa(); const router = new Router(); - router.use(function(ctx, next) { + router.use(function(_ctx, next) { return next(); }); @@ -1046,19 +884,14 @@ describe('test/lib/router.test.js', function() { }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/foo/815') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('id', '815'); - expect(res.body).to.not.have.property('0'); - done(); - }); + .expect(200); + assert.equal(res.body.id, '815'); + assert.equal(res.body['0'], undefined); }); - it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', function(done) { + it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', async () => { const app = new Koa(); const router = new Router(); const nested = new Router(); @@ -1079,78 +912,66 @@ describe('test/lib/router.test.js', function() { router.use(nested.routes()); app.use(router.routes()); - request(app.callback()) + await request(app.callback()) .get('/test') .expect(200) - .expect('test') - .end(function(err) { - if (err) return done(err); - expect(called).to.eql(1, 'too many routes matched'); - done(); - }); + .expect('test'); + assert.equal(called, 1); }); }); - describe('Router#register()', function() { - it('registers new routes', function(done) { + describe('Router#register()', () => { + it('registers new routes', () => { const app = new Koa(); const router = new Router(); - router.should.have.property('register'); - router.register.should.be.type('function'); + assert(typeof router.register === 'function'); const route = router.register('/', [ 'GET', 'POST' ], function() {}); - should.exists(route); + assert(route); app.use(router.routes()); - router.stack.should.be.an.instanceOf(Array); - router.stack.should.have.property('length', 1); - router.stack[0].should.have.property('path', '/'); - done(); + assert.equal(router.stack.length, 1); + assert.equal(router.stack[0].path, '/'); }); }); - describe('Router#redirect()', function() { - it('registers redirect routes', function(done) { + describe('Router#redirect()', () => { + it('registers redirect routes', () => { const app = new Koa(); const router = new Router(); - router.should.have.property('redirect'); - router.redirect.should.be.type('function'); + assert(typeof router.redirect === 'function'); router.redirect('/source', '/destination', 302); app.use(router.routes()); - router.stack.should.have.property('length', 1); - router.stack[0].should.be.instanceOf(Layer); - router.stack[0].should.have.property('path', '/source'); - done(); + assert.equal(router.stack.length, 1); + assert.equal(router.stack[0].path, '/source'); }); - it('redirects using route names', function(done) { + it('redirects using route names', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get('home', '/', function() {}); router.get('sign-up-form', '/sign-up-form', function() {}); router.redirect('home', 'sign-up-form'); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .post('/') - .expect(301) - .end(function(err, res) { - if (err) return done(err); - res.header.should.have.property('location', '/sign-up-form'); - done(); - }); + .expect(301); + assert.equal(res.headers.location, '/sign-up-form'); }); }); - describe('Router#route()', function() { - it('inherits routes from nested router', function() { + describe('Router#route()', () => { + it('inherits routes from nested router', () => { const subrouter = new Router().get('child', '/hello', function(ctx) { ctx.body = { hello: 'world' }; }); const router = new Router().use(subrouter.routes()); - expect(router.route('child')).to.have.property('name', 'child'); + const route = router.route('child'); + assert(route); + assert.equal(route.name, 'child'); }); }); - describe('Router#url()', function() { - it('generates URL for given route name', function(done) { + describe('Router#url()', () => { + it('generates URL for given route name', () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); @@ -1158,16 +979,16 @@ describe('test/lib/router.test.js', function() { ctx.status = 204; }); let url = router.url('books', { category: 'programming', title: 'how to node' }); - url.should.equal('/programming/how%20to%20node'); + assert.equal(url, '/programming/how%20to%20node'); url = router.url('books', 'programming', 'how to node'); - url.should.equal('/programming/how%20to%20node'); + assert.equal(url, '/programming/how%20to%20node'); const err = router.url('not-exists', { category: 'programming', title: 'how to node' }); - err.message.should.equal('No route found for name: not-exists'); - done(); + assert(err instanceof Error); + assert.equal(err.message, 'No route found for name: not-exists'); }); - it('generates URL for given route name within embedded routers', function(done) { + it('generates URL for given route name within embedded routers', () => { const app = new Koa(); const router = new Router({ prefix: '/books', @@ -1182,13 +1003,12 @@ describe('test/lib/router.test.js', function() { router.use(embeddedRouter.routes()); app.use(router.routes()); let url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 }); - url.should.equal('/books/chapters/Learning%20ECMA6/123'); + assert.equal(url, '/books/chapters/Learning%20ECMA6/123'); url = router.url('chapters', 'Learning ECMA6', 123); - url.should.equal('/books/chapters/Learning%20ECMA6/123'); - done(); + assert.equal(url, '/books/chapters/Learning%20ECMA6/123'); }); - it('generates URL for given route name within two embedded routers', function(done) { + it('generates URL for given route name within two embedded routers', () => { const app = new Koa(); const router = new Router({ prefix: '/books', @@ -1206,11 +1026,10 @@ describe('test/lib/router.test.js', function() { router.use(embeddedRouter.routes()); app.use(router.routes()); const url = router.url('chapters', { chapterName: 'Learning ECMA6', pageNumber: 123 }); - url.should.equal('/books/chapters/Learning%20ECMA6/pages/123'); - done(); + assert.equal(url, '/books/chapters/Learning%20ECMA6/pages/123'); }); - it('generates URL for given route name with params and query params', function(done) { + it('generates URL for given route name with params and query params', () => { const router = new Router(); router.get('books', '/books/:category/:id', function(ctx) { ctx.status = 204; @@ -1218,21 +1037,20 @@ describe('test/lib/router.test.js', function() { let url = router.url('books', 'programming', 4, { query: { page: 3, limit: 10 }, }); - url.should.equal('/books/programming/4?page=3&limit=10'); + assert.equal(url, '/books/programming/4?page=3&limit=10'); url = router.url('books', { category: 'programming', id: 4 }, - { query: { page: 3, limit: 10 } } + { query: { page: 3, limit: 10 } }, ); - url.should.equal('/books/programming/4?page=3&limit=10'); + assert.equal(url, '/books/programming/4?page=3&limit=10'); url = router.url('books', { category: 'programming', id: 4 }, - { query: 'page=3&limit=10' } + { query: 'page=3&limit=10' }, ); - url.should.equal('/books/programming/4?page=3&limit=10'); - done(); + assert.equal(url, '/books/programming/4?page=3&limit=10'); }); - it('generates URL for given route name without params and query params', function(done) { + it('generates URL for given route name without params and query params', () => { const router = new Router(); router.get('category', '/category', function(ctx) { ctx.status = 204; @@ -1240,13 +1058,12 @@ describe('test/lib/router.test.js', function() { const url = router.url('category', { query: { page: 3, limit: 10 }, }); - url.should.equal('/category?page=3&limit=10'); - done(); + assert.equal(url, '/category?page=3&limit=10'); }); }); - describe('Router#param()', function() { - it('runs parameter middleware', function(done) { + describe('Router#param()', () => { + it('runs parameter middleware', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); @@ -1262,18 +1079,13 @@ describe('test/lib/router.test.js', function() { .get('/users/:user', function(ctx) { ctx.body = ctx.user; }); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/users/3') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + .expect(200); + assert.equal(res.body.name, 'alex'); }); - it('runs parameter middleware in order of URL appearance', function(done) { + it('runs parameter middleware in order of URL appearance', async () => { const app = new Koa(); const router = new Router(); router @@ -1303,22 +1115,14 @@ describe('test/lib/router.test.js', function() { ctx.body = ctx.user; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + const res = await request(app.use(router.routes()).callback()) .get('/first/users/3') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - res.body.should.have.property('ordered', 'parameters'); - done(); - }); + .expect(200); + assert.equal(res.body.name, 'alex'); + assert.equal(res.body.ordered, 'parameters'); }); - it('runs parameter middleware in order of URL appearance even when added in random order', function(done) { + it('runs parameter middleware in order of URL appearance even when added in random order', async () => { const app = new Koa(); const router = new Router(); router @@ -1343,21 +1147,13 @@ describe('test/lib/router.test.js', function() { ctx.body = ctx.state.loaded; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + const res = await request(app.use(router.routes()).callback()) .get('/1/2/3/4') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.eql([ '1', '2', '3', '4' ]); - done(); - }); + .expect(200); + assert.deepEqual(res.body, [ '1', '2', '3', '4' ]); }); - it('runs parent parameter middleware for subrouter', function(done) { + it('runs parent parameter middleware for subrouter', async () => { const app = new Koa(); const router = new Router(); const subrouter = new Router(); @@ -1378,21 +1174,16 @@ describe('test/lib/router.test.js', function() { }) .use('/:id/children', subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request(app.use(router.routes()).callback()) .get('/did-not-run/children/2') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('id', 'ran'); - res.body.should.have.property('cid', '2'); - done(); - }); + .expect(200); + assert.deepEqual(res.body.id, 'ran'); + assert.deepEqual(res.body.cid, '2'); }); }); - describe('Router#opts', function() { - it('responds with 200', function(done) { + describe('Router#opts', () => { + it('responds with 200', async () => { const app = new Koa(); const router = new Router({ strict: true, @@ -1400,20 +1191,13 @@ describe('test/lib/router.test.js', function() { router.get('/info', function(ctx) { ctx.body = 'hello'; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + const res = await request(app.use(router.routes()).callback()) .get('/info') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.text.should.equal('hello'); - done(); - }); + .expect(200); + assert.equal(res.text, 'hello'); }); - it('should allow setting a prefix', function(done) { + it('should allow setting a prefix', async () => { const app = new Koa(); const routes = new Router({ prefix: '/things/:thing_id' }); @@ -1421,19 +1205,13 @@ describe('test/lib/router.test.js', function() { ctx.body = ctx.params; }); - app.use(routes.routes()); - - request(http.createServer(app.callback())) + const res = await request(app.use(routes.routes()).callback()) .get('/things/1/list') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.body.thing_id.should.equal('1'); - done(); - }); + .expect(200); + assert.equal(res.body.thing_id, '1'); }); - it('responds with 404 when has a trailing slash', function(done) { + it('responds with 404 when has a trailing slash', async () => { const app = new Koa(); const router = new Router({ strict: true, @@ -1441,21 +1219,14 @@ describe('test/lib/router.test.js', function() { router.get('/info', function(ctx) { ctx.body = 'hello'; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + await request(app.use(router.routes()).callback()) .get('/info/') - .expect(404) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(404); }); }); - describe('use middleware with opts', function() { - it('responds with 200', function(done) { + describe('use middleware with opts', () => { + it('responds with 200', async () => { const app = new Koa(); const router = new Router({ strict: true, @@ -1463,20 +1234,13 @@ describe('test/lib/router.test.js', function() { router.get('/info', function(ctx) { ctx.body = 'hello'; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + const res = await request(app.use(router.routes()).callback()) .get('/info') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - res.text.should.equal('hello'); - done(); - }); + .expect(200); + assert.equal(res.text, 'hello'); }); - it('responds with 404 when has a trailing slash', function(done) { + it('responds with 404 when has a trailing slash', async () => { const app = new Koa(); const router = new Router({ strict: true, @@ -1484,219 +1248,163 @@ describe('test/lib/router.test.js', function() { router.get('/info', function(ctx) { ctx.body = 'hello'; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + await request(app.use(router.routes()).callback()) .get('/info/') - .expect(404) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(404); }); }); - describe('router.routes()', function() { - it('should return composed middleware', function(done) { + describe('router.routes()', () => { + it('should return composed middleware', async () => { const app = new Koa(); const router = new Router(); let middlewareCount = 0; - const middlewareA = function(ctx, next) { + const middlewareA = function(_ctx: any, next: Next) { middlewareCount++; return next(); }; - const middlewareB = function(ctx, next) { + const middlewareB = function(_ctx: any, next: Next) { middlewareCount++; return next(); }; router.use(middlewareA, middlewareB); router.get('/users/:id', function(ctx) { - should.exist(ctx.params.id); + assert(ctx.params.id); ctx.body = { hello: 'world' }; }); const routerMiddleware = router.routes(); + assert(typeof routerMiddleware === 'function'); - expect(routerMiddleware).to.be.a('function'); - - request(http.createServer( - app - .use(routerMiddleware) - .callback())) + const res = await request(app.use(routerMiddleware).callback()) .get('/users/1') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('hello', 'world'); - expect(middlewareCount).to.equal(2); - done(); - }); + .expect(200); + assert.equal(res.body.hello, 'world'); + assert.equal(middlewareCount, 2); }); - it('places a `_matchedRoute` value on context', function(done) { + it('places a `_matchedRoute` value on context', async () => { const app = new Koa(); const router = new Router(); - const middleware = function(ctx, next) { - expect(ctx._matchedRoute).to.be('/users/:id'); + const middleware = function(ctx: any, next: Next) { + assert.equal(ctx._matchedRoute, '/users/:id'); return next(); }; router.get('/users/:id', middleware, function(ctx) { - expect(ctx._matchedRoute).to.be('/users/:id'); - should.exist(ctx.params.id); + assert.equal(ctx._matchedRoute, '/users/:id'); + assert(ctx.params.id); ctx.body = { hello: 'world' }; }); const routerMiddleware = router.routes(); - request(http.createServer( - app - .use(routerMiddleware) - .callback())) + await request(app.use(routerMiddleware).callback()) .get('/users/1') - .expect(200) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(200); }); - it('places a `_matchedRouteName` value on the context for a named route', function(done) { + it('places a `_matchedRouteName` value on the context for a named route', async () => { const app = new Koa(); const router = new Router(); router.get('users#show', '/users/:id', function(ctx) { - expect(ctx._matchedRouteName).to.be('users#show'); + assert.equal(ctx._matchedRouteName, 'users#show'); ctx.status = 200; }); - request(http.createServer(app.use(router.routes()).callback())) + await request(app.use(router.routes()).callback()) .get('/users/1') - .expect(200) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(200); }); - it('does not place a `_matchedRouteName` value on the context for unnamed routes', function(done) { + it('does not place a `_matchedRouteName` value on the context for unnamed routes', async () => { const app = new Koa(); const router = new Router(); router.get('/users/:id', function(ctx) { - expect(ctx._matchedRouteName).to.be(undefined); + assert.equal(ctx._matchedRouteName, undefined); ctx.status = 200; }); - request(http.createServer(app.use(router.routes()).callback())) + await request(app.use(router.routes()).callback()) .get('/users/1') - .expect(200) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(200); }); - it('routerName and routerPath work with next', function(done) { + it('routerName and routerPath work with next', async () => { const app = new Koa(); const router = new Router(); router.get('name1', '/users/1', function(ctx, next) { - expect(ctx._matchedRouteName).to.be('name1'); - expect(ctx.routerName).to.be('name1'); - expect(ctx._matchedRoute).to.be('/users/1'); - expect(ctx.routerPath).to.be('/users/1'); + assert.equal(ctx._matchedRouteName, 'name1'); + assert.equal(ctx.routerName, 'name1'); + assert.equal(ctx._matchedRoute, '/users/1'); + assert.equal(ctx.routerPath, '/users/1'); return next(); }); router.get('name2', '/users/:id', function(ctx) { - expect(ctx._matchedRouteName).to.be('name2'); - expect(ctx.routerName).to.be('name2'); - expect(ctx._matchedRoute).to.be('/users/:id'); - expect(ctx.routerPath).to.be('/users/:id'); + assert.equal(ctx._matchedRouteName, 'name2'); + assert.equal(ctx.routerName, 'name2'); + assert.equal(ctx._matchedRoute, '/users/:id'); + assert.equal(ctx.routerPath, '/users/:id'); ctx.status = 200; }); - request(http.createServer(app.use(router.routes()).callback())) + await request(app.use(router.routes()).callback()) .get('/users/1') - .expect(200) - .end(function(err) { - if (err) return done(err); - done(); - }); + .expect(200); }); }); - describe('If no HEAD method, default to GET', function() { - it('should default to GET', function(done) { - const app = new Koa(); - const router = new Router(); - router.get('/users/:id', function(ctx) { - should.exist(ctx.params.id); - ctx.body = 'hello'; - }); - request(http.createServer( - app - .use(router.routes()) - .callback())) - .head('/users/1') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.be.empty(); - done(); - }); - }); - - it('should work with middleware', function(done) { + describe('If no HEAD method, default to GET', () => { + it('should default to GET', async () => { const app = new Koa(); const router = new Router(); + console.log(router); router.get('/users/:id', function(ctx) { - should.exist(ctx.params.id); + assert(ctx.params.id); ctx.body = 'hello'; }); - request(http.createServer( - app - .use(router.routes()) - .callback())) + app.use(router.routes()); + let res = await request(app.callback()) + .get('/users/1') + .expect(200); + assert.equal(res.text, 'hello'); + res = await request(app.callback()) .head('/users/1') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(res.body).to.be.empty(); - done(); - }); + .expect(200); + assert.equal(res.text, ''); }); }); - describe('Router#prefix', function() { - it('should set opts.prefix', function() { + describe('Router#prefix', () => { + it('should set opts.prefix', () => { const router = new Router(); - expect(router.opts).to.not.have.key('prefix'); + assert.equal(router.opts.prefix, undefined); router.prefix('/things/:thing_id'); - expect(router.opts.prefix).to.equal('/things/:thing_id'); + assert.equal(router.opts.prefix, '/things/:thing_id'); }); - it('should prefix existing routes', function() { + it('should prefix existing routes', () => { const router = new Router(); router.get('/users/:id', function(ctx) { ctx.body = 'test'; }); router.prefix('/things/:thing_id'); const route = router.stack[0]; - expect(route.path).to.equal('/things/:thing_id/users/:id'); - expect(route.paramNames).to.have.length(2); - expect(route.paramNames[0]).to.have.property('name', 'thing_id'); - expect(route.paramNames[1]).to.have.property('name', 'id'); + assert.equal(route.path, '/things/:thing_id/users/:id'); + assert.equal(route.paramNames.length, 2); + assert.equal(route.paramNames[0].name, 'thing_id'); + assert.equal(route.paramNames[1].name, 'id'); }); - describe('when used with .use(fn) - gh-247', function() { - it('does not set params.0 to the matched path', function(done) { + describe('when used with .use(fn) - gh-247', () => { + it('does not set params.0 to the matched path', async () => { const app = new Koa(); const router = new Router(); - router.use(function(ctx, next) { + router.use(function(_ctx, next) { return next(); }); @@ -1707,25 +1415,20 @@ describe('test/lib/router.test.js', function() { router.prefix('/things'); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(app.callback()) .get('/things/foo/108') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('id', '108'); - expect(res.body).to.not.have.property('0'); - done(); - }); + .expect(200); + assert.equal(res.body.id, '108'); + assert.equal(res.body['0'], undefined); }); }); describe('with trailing slash', testPrefix('/admin/')); describe('without trailing slash', testPrefix('/admin')); - function testPrefix(prefix) { - return function() { - let server; + function testPrefix(prefix: string) { + return () => { + let server: any; let middlewareCount = 0; before(function() { @@ -1744,94 +1447,73 @@ describe('test/lib/router.test.js', function() { }); router.prefix(prefix); - server = http.createServer(app.use(router.routes()).callback()); - }); - - after(function() { - server.close(); + server = app.use(router.routes()).callback(); }); - beforeEach(function() { + beforeEach(() => { middlewareCount = 0; }); - it('should support root level router middleware', function(done) { - request(server) + it('should support root level router middleware', async () => { + const res = await request(server) .get(prefix) - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); + .expect(200); + assert.equal(middlewareCount, 2); + assert.equal(res.body.name, 'worked'); }); - it('should support requests with a trailing path slash', function(done) { - request(server) + it('should support requests with a trailing path slash', async () => { + const res = await request(server) .get('/admin/') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); + .expect(200); + assert.equal(middlewareCount, 2); + assert.equal(res.body.name, 'worked'); }); - it('should support requests without a trailing path slash', function(done) { - request(server) + it('should support requests without a trailing path slash', async () => { + const res = await request(server) .get('/admin') - .expect(200) - .end(function(err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); + .expect(200); + assert.equal(middlewareCount, 2); + assert.equal(res.body.name, 'worked'); }); }; } }); - describe('Static Router#url()', function() { - it('generates route URL', function() { + describe('Static Router#url()', () => { + it('generates route URL', () => { const url = Router.url('/:category/:title', { category: 'programming', title: 'how-to-node' }); - url.should.equal('/programming/how-to-node'); + assert.equal(url, '/programming/how-to-node'); }); - it('escapes using encodeURIComponent()', function() { + it('escapes using encodeURIComponent()', () => { const url = Router.url('/:category/:title', { category: 'programming', title: 'how to node' }); - url.should.equal('/programming/how%20to%20node'); + assert.equal(url, '/programming/how%20to%20node'); }); - it('generates route URL with params and query params', function(done) { + it('generates route URL with params and query params', () => { let url = Router.url('/books/:category/:id', 'programming', 4, { query: { page: 3, limit: 10 }, }); - url.should.equal('/books/programming/4?page=3&limit=10'); + assert.equal(url, '/books/programming/4?page=3&limit=10'); url = Router.url('/books/:category/:id', { category: 'programming', id: 4 }, - { query: { page: 3, limit: 10 } } + { query: { page: 3, limit: 10 } }, ); - url.should.equal('/books/programming/4?page=3&limit=10'); + assert.equal(url, '/books/programming/4?page=3&limit=10'); url = Router.url('/books/:category/:id', { category: 'programming', id: 4 }, - { query: 'page=3&limit=10' } + { query: 'page=3&limit=10' }, ); - url.should.equal('/books/programming/4?page=3&limit=10'); - done(); + assert.equal(url, '/books/programming/4?page=3&limit=10'); }); - it('generates router URL without params and with with query params', function(done) { + it('generates router URL without params and with with query params', () => { const url = Router.url('/category', { query: { page: 3, limit: 10 }, }); - url.should.equal('/category?page=3&limit=10'); - done(); + assert.equal(url, '/category?page=3&limit=10'); }); }); }); diff --git a/test/index.test.ts b/test/index.test.ts index 837e500..71c2b15 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; -import Router, { KoaRouter } from '../src/index.js'; +import Router, { KoaRouter, EggRouter } from '../src/index.js'; describe('test/index.test.ts', () => { it('should expose Router', () => { assert(typeof Router === 'function'); assert(typeof KoaRouter === 'function'); - // assert(typeof Router.EggRouter === 'function'); + assert.equal(Router, KoaRouter); + assert(typeof EggRouter === 'function'); }); }); diff --git a/test/utils.test.ts b/test/utils.test.ts deleted file mode 100644 index a6c6f4c..0000000 --- a/test/utils.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -const utils = require('../../lib/utils'); -const is = require('is-type-of'); -const assert = require('assert'); - -describe('test/lib/utils.test.js', () => { - describe('callFn', () => { - it('should not function return same', () => { - const res = utils.callFn('foo'); - assert(is.promise(res)); - return res.then(result => assert(result === undefined)); - }); - - it('should async function return promise', () => { - const res = utils.callFn(async (foo, bar) => { - return foo + bar; - }, [ 1, 2 ]); - assert(is.promise(res)); - return res.then(result => assert(result === 3)); - }); - - it('should generator function return promise', () => { - const res = utils.callFn(function* (foo, bar) { - return foo + bar; - }, [ 1, 2 ]); - assert(is.promise(res)); - return res.then(result => assert(result === 3)); - }); - - it('should common function return promise', () => { - const res = utils.callFn((foo, bar) => { - return foo + bar; - }, [ 1, 2 ]); - assert(is.promise(res)); - return res.then(result => assert(result === 3)); - }); - - it('should work with ctx', () => { - const res = utils.callFn(async function(bar) { - return this.foo + bar; - }, [ 2 ], { foo: 1 }); - assert(is.promise(res)); - return res.then(result => assert(result === 3)); - }); - }); - - describe('middleware', () => { - it('should work with async function', () => { - const res = utils.middleware(async () => {}); - assert(is.asyncFunction(res)); - }); - - it('should work with generator function', () => { - const res = utils.middleware(function* () { - return; - }); - assert(!is.generatorFunction(res)); - }); - }); -}); From 711543277d28c0ad3b5746dd5026a383de757366 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Tue, 11 Jun 2024 14:35:20 +0800 Subject: [PATCH 10/13] f --- src/EggRouter.ts | 87 ++++++++++++++++++------------------------ src/Router.ts | 26 ++++++------- test/EggRouter.test.ts | 25 ++++++++++++ 3 files changed, 74 insertions(+), 64 deletions(-) diff --git a/src/EggRouter.ts b/src/EggRouter.ts index f5e38b6..965bce0 100644 --- a/src/EggRouter.ts +++ b/src/EggRouter.ts @@ -72,30 +72,11 @@ export class EggRouter extends Router { this.app = app; } - #formatRouteParams(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController, - middlewares: (MiddlewareFunc | string | ResourcesController)[]) { - const options: RegisterOptions = {}; - let path: string | RegExp; - if (typeof pathOrMiddleware === 'string' || pathOrMiddleware instanceof RegExp) { - // verb(method, name, path, ...middlewares) - path = pathOrMiddleware; - assert(typeof nameOrPath === 'string', 'route name should be string'); - options.name = nameOrPath; - } else { - // verb(method, path, ...middlewares) - path = nameOrPath; - middlewares = [ pathOrMiddleware, ...middlewares ]; - } - return { - path, - middlewares, - options, - }; - } - - verb(method: RouterMethod | RouterMethod[], nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + verb(method: RouterMethod | RouterMethod[], + nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middleware: (MiddlewareFunc | string)[]) { - const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware); + const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware); if (typeof method === 'string') { method = [ method ]; } @@ -104,56 +85,64 @@ export class EggRouter extends Router { } // const METHODS = [ 'head', 'options', 'get', 'put', 'patch', 'post', 'delete', 'all' ]; - head(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - head(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - head(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + head(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + head(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + head(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('head', nameOrPath, pathOrMiddleware, ...middlewares); } - options(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - options(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - options(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + options(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + options(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + options(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('options', nameOrPath, pathOrMiddleware, ...middlewares); } - get(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - get(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - get(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + get(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + get(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + get(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('get', nameOrPath, pathOrMiddleware, ...middlewares); } - put(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - put(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - put(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + put(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + put(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + put(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('put', nameOrPath, pathOrMiddleware, ...middlewares); } - patch(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - patch(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - patch(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + patch(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + patch(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + patch(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('patch', nameOrPath, pathOrMiddleware, ...middlewares); } - post(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - post(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - post(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + post(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + post(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + post(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('post', nameOrPath, pathOrMiddleware, ...middlewares); } - delete(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - delete(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - delete(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + delete(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + delete(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + delete(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb('delete', nameOrPath, pathOrMiddleware, ...middlewares); } - all(path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - all(name: string, path: string | RegExp, ...middlewares: (MiddlewareFunc | string)[]): Router; - all(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc, + all(path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + all(name: string, path: string | RegExp | (string | RegExp)[], ...middlewares: (MiddlewareFunc | string)[]): Router; + all(nameOrPath: string | RegExp | (string | RegExp)[], + pathOrMiddleware: string | RegExp | (string | RegExp)[] | MiddlewareFunc, ...middlewares: (MiddlewareFunc | string)[]): Router { return this.verb(methods, nameOrPath, pathOrMiddleware, ...middlewares); } - register(path: string | string[] | RegExp | RegExp[], + register(path: string | RegExp | (string | RegExp)[], methods: string[], middleware: MiddlewareFunc | string | (MiddlewareFunc | string | ResourcesController)[], opts?: RegisterOptions) { @@ -213,7 +202,7 @@ export class EggRouter extends Router { resources(name: string, prefix: string, middleware: MiddlewareFunc, controller: string | ResourcesController): Router; resources(nameOrPath: string | RegExp, pathOrMiddleware: string | RegExp | MiddlewareFunc | ResourcesController, ...middleware: (MiddlewareFunc | string | ResourcesController)[]): Router { - const { path, middlewares, options } = this.#formatRouteParams(nameOrPath, pathOrMiddleware, middleware); + const { path, middlewares, options } = this._formatRouteParams(nameOrPath, pathOrMiddleware, middleware); // last argument is Controller object const controller = resolveController(middlewares.pop()!, this.app); for (const key in REST_MAP) { diff --git a/src/Router.ts b/src/Router.ts index b92527a..5400b0e 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -401,17 +401,26 @@ export class Router { methods: string[], middleware: MiddlewareFunc | MiddlewareFunc[], opts?: RegisterOptions): Layer | Layer[] { - opts = opts ?? {}; // support array of paths if (Array.isArray(path)) { const routes: Layer[] = []; for (const p of path) { - const route = this.register(p, methods, middleware, opts) as Layer; + const route = this.#register(p, methods, middleware, opts); routes.push(route); } return routes; } + // create route + const route = this.#register(path, methods, middleware, opts); + return route; + } + + #register(path: string | RegExp, + methods: string[], + middleware: MiddlewareFunc | MiddlewareFunc[], + opts?: RegisterOptions): Layer { + opts = opts ?? {}; // create route const route = new Layer(path, methods, middleware, { end: opts.end === false ? opts.end : true, @@ -478,12 +487,6 @@ export class Router { * router.url('user', { id: 3 }, { query: "limit=1" }); * // => "/users/3?limit=1" * ``` - * - * @param {String} name route name - * @param {Object} params url parameters - * @param {Object} [options] options parameter - * @param {Object|String} [options.query] query options - * @return {String|Error} string or error instance */ url(name: string, params?: string | number | object, ...paramsOrOptions: (string | number | object | LayerURLOptions)[]): string | Error { @@ -733,13 +736,6 @@ export class Router { * The [path-to-regexp](https://github.com/pillarjs/path-to-regexp) module is * used to convert paths to regular expressions. * - * @name get|put|post|patch|delete|del - * @memberof module:koa-router.prototype - * @param {String} method http method - * @param {String} nameOrPath http path - * @param {Function=} pathOrMiddleware route middleware(s) - * @param {Function} middlewares middlewares - * @return {Router} Router instance */ verb(method: string | string[], nameOrPath: string | RegExp | (string | RegExp)[], diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts index 1dec08e..bdef34a 100644 --- a/test/EggRouter.test.ts +++ b/test/EggRouter.test.ts @@ -55,6 +55,31 @@ describe('test/EggRouter.test.ts', () => { assert(router.stack[1].stack.length === 1); }); + it('should app.verb([url1, url2], controller) work', () => { + const app = { + controller: { + async foo() { return; }, + hello: { + world() { return; }, + }, + }, + }; + + const router = new EggRouter({}, app); + router.get([ '/foo', '/bar' ], app.controller.foo); + router.post('/hello/world', app.controller.hello.world); + + assert(router.stack[0].path === '/foo'); + assert.deepEqual(router.stack[0].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[0].stack.length === 1); + assert(router.stack[1].path === '/bar'); + assert.deepEqual(router.stack[1].methods, [ 'HEAD', 'GET' ]); + assert(router.stack[2].stack.length === 1); + assert(router.stack[2].path === '/hello/world'); + assert.deepEqual(router.stack[2].methods, [ 'POST' ]); + assert(router.stack[2].stack.length === 1); + }); + it('should app.verb(name, url, controller) work', () => { const app = { controller: { From 38b0bad24a3414b0aa75c994542ceef865eac4cc Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Tue, 11 Jun 2024 14:40:25 +0800 Subject: [PATCH 11/13] f --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 7595ad4..0967490 100644 --- a/README.md +++ b/README.md @@ -49,14 +49,14 @@ Create a new router. **Example** Basic usage: -```javascript -var Koa = require('koa'); -var Router = require('@eggjs/router'); +```ts +import Koa from '@eggjs/koa'; +import Router from '@eggjs/router'; -var app = new Koa(); -var router = new Router(); +const app = new Koa(); +const router = new Router(); -router.get('/', (ctx, next) => { +router.get('/', async (ctx, next) => { // ctx.router available }); @@ -77,7 +77,7 @@ where **verb** is one of the HTTP verbs such as `router.get()` or `router.post() Additionaly, `router.all()` can be used to match against all methods. -```javascript +```ts router .get('/', (ctx, next) => { ctx.body = 'Hello World!'; @@ -109,7 +109,7 @@ Query strings will not be considered when matching requests. Routes can optionally have names. This allows generation of URLs and easy renaming of URLs during development. -```javascript +```ts router.get('user', '/users/:id', (ctx, next) => { // ... }); @@ -122,7 +122,7 @@ router.url('user', 3); Multiple middleware may be given: -```javascript +```ts router.get( '/users/:id', (ctx, next) => { @@ -142,9 +142,9 @@ router.get( Nesting routers is supported: -```javascript -var forums = new Router(); -var posts = new Router(); +```ts +const forums = new Router(); +const posts = new Router(); posts.get('/', (ctx, next) => {...}); posts.get('/:pid', (ctx, next) => {...}); @@ -158,8 +158,8 @@ app.use(forums.routes()); Route paths can be prefixed at the router level: -```javascript -var router = new Router({ +```ts +const router = new Router({ prefix: '/users' }); @@ -171,7 +171,7 @@ router.get('/:id', ...); // responds to "/users/:id" Named route parameters are captured and added to `ctx.params`. -```javascript +```ts router.get('/:category/:title', (ctx, next) => { console.log(ctx.params); // => { category: 'programming', title: 'how-to-node' } @@ -215,7 +215,7 @@ sequentially, requests start at the first middleware and work their way **Example** -```javascript +```ts // session middleware will run before authorize router .use(session()) @@ -244,7 +244,7 @@ Set the path prefix for a Router instance that was already initialized. **Example** -```javascript +```ts router.prefix('/things/:thing_id') ``` @@ -267,12 +267,12 @@ with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. **Example** -```javascript -var Koa = require('koa'); -var Router = require('egg-router'); +```ts +import Koa from '@eggjs/koa'; +import Router from '@eggjs/router'; -var app = new Koa(); -var router = new Router(); +const app = new Koa(); +const router = new Router(); app.use(router.routes()); app.use(router.allowedMethods()); @@ -280,13 +280,13 @@ app.use(router.allowedMethods()); **Example with [Boom](https://github.com/hapijs/boom)** -```javascript -var Koa = require('koa'); -var Router = require('egg-router'); -var Boom = require('boom'); +```ts +import Koa from '@eggjs/koa'; +import Router from '@eggjs/router'; +import Boom from 'boom'; -var app = new Koa(); -var router = new Router(); +const app = new Koa(); +const router = new Router(); app.use(router.routes()); app.use(router.allowedMethods({ @@ -310,7 +310,7 @@ router.redirect('/login', 'sign-in'); This is equivalent to: -```javascript +```ts router.all('/login', ctx => { ctx.redirect('/sign-in'); ctx.status = 301; @@ -354,7 +354,7 @@ Generate URL for route. Takes a route name and map of named `params`. **Example** -```javascript +```ts router.get('user', '/users/:id', (ctx, next) => { // ... }); @@ -393,7 +393,7 @@ validation. **Example** -```javascript +```ts router .param('user', (id, ctx, next) => { ctx.user = users[id]; @@ -429,8 +429,8 @@ Generate URL from url pattern and given `params`. **Example** -```javascript -var url = Router.url('/users/:id', {id: 1}); +```ts +const url = Router.url('/users/:id', {id: 1}); // => "/users/1" const url = Router.url('/users/:id', {id: 1}, {query: { active: true }}); From ad4cd64acc9b08d88f0bd5d94f458cb187d6c054 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Tue, 11 Jun 2024 15:03:44 +0800 Subject: [PATCH 12/13] f --- bench/run | 2 +- bench/server.cjs | 10 +++++----- package.json | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bench/run b/bench/run index 9cd8782..b1892f8 100755 --- a/bench/run +++ b/bench/run @@ -8,7 +8,7 @@ export PORT=3333 host="http://localhost:$PORT" -node "$(dirname $0)/server.js" & +node "$(dirname $0)/server.cjs" & pid=$! diff --git a/bench/server.cjs b/bench/server.cjs index 4d4dbdf..adb5e07 100644 --- a/bench/server.cjs +++ b/bench/server.cjs @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const Koa = require('@eggjs/koa'); -const Router = require('../dist/commonjs'); +const { Application } = require('@eggjs/koa'); +const { Router } = require('../dist/commonjs'); -const app = new Koa(); +const app = new Application(); const router = new Router(); const ok = ctx => { @@ -35,8 +35,8 @@ for (let i = n; i > 0; i--) { const child = new Router(); if (useMiddleware) child.use((ctx, next) => next()); child.get(`/:${''.padStart(i, 'a')}`, ok); - child.nest('/grandchild', grandchild); - router.nest(`/${i}/child`, child); + // child.use('/grandchild', grandchild); + // router.use(`/${i}/child`, child); } if (process.env.DEBUG) { diff --git a/package.json b/package.json index 149e16b..a56a0bd 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "utility": "^2.1.0" }, "devDependencies": { - "@eggjs/koa": "^2.18.0", + "@eggjs/koa": "^2.18.1", "@eggjs/tsconfig": "^1.3.3", "@types/koa-compose": "^3.2.8", "@types/methods": "^1.1.4", @@ -57,7 +57,8 @@ "preci": "npm run lint && npm run prepublishOnly", "ci": "egg-bin cov", "contributor": "git-contributor", - "prepublishOnly": "tshy && tshy-after" + "prepublishOnly": "tshy && tshy-after", + "bench": "cd bench && make" }, "license": "MIT", "files": [ From edf455d3e2c6bb8ae2328701cbd113a045f84213 Mon Sep 17 00:00:00 2001 From: fengmk2 <suqian.yf@antgroup.com> Date: Tue, 11 Jun 2024 15:13:00 +0800 Subject: [PATCH 13/13] f --- test/EggRouter.test.ts | 7 +++++++ test/Router.test.ts | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/test/EggRouter.test.ts b/test/EggRouter.test.ts index bdef34a..353df64 100644 --- a/test/EggRouter.test.ts +++ b/test/EggRouter.test.ts @@ -53,6 +53,13 @@ describe('test/EggRouter.test.ts', () => { assert(router.stack[1].path === '/hello/world'); assert.deepEqual(router.stack[1].methods, [ 'POST' ]); assert(router.stack[1].stack.length === 1); + + router.head('/foo-head', app.controller.foo); + router.options('/foo-options', app.controller.foo); + router.put('/foo-put', app.controller.foo); + router.patch('/foo-patch', app.controller.foo); + router.delete('/foo-delete', app.controller.foo); + router.all('/foo-all', app.controller.foo); }); it('should app.verb([url1, url2], controller) work', () => { diff --git a/test/Router.test.ts b/test/Router.test.ts index d32f68d..b10a137 100644 --- a/test/Router.test.ts +++ b/test/Router.test.ts @@ -956,6 +956,17 @@ describe('test/lib/router.test.js', () => { .expect(301); assert.equal(res.headers.location, '/sign-up-form'); }); + + it('registers redirect not exists routes', () => { + const router = new Router(); + assert(typeof router.redirect === 'function'); + assert.throws(() => { + router.redirect('source-not-exists', '/destination', 302); + }, /Error: No route found for name: source-not-exists/); + assert.throws(() => { + router.redirect('/source', 'destination-not-exists'); + }, /Error: No route found for name: destination-not-exists/); + }); }); describe('Router#route()', () => {