diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b52cfc9..5062ae8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,27 +34,32 @@ jobs: - name: Node.js 0.8 node-version: "0.8" npm-i: mocha@2.5.3 supertest@1.1.0 - npm-rm: nyc + npm-rm: nyc typescript @types/node - name: Node.js 0.10 node-version: "0.10" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-rm: typescript @types/node - name: Node.js 0.12 node-version: "0.12" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-rm: typescript @types/node - name: io.js 1.x node-version: "1.8" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-rm: typescript @types/node - name: io.js 2.x node-version: "2.5" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-rm: typescript @types/node - name: io.js 3.x node-version: "3.3" npm-i: mocha@3.5.3 nyc@10.3.2 supertest@2.0.0 + npm-rm: typescript @types/node - name: Node.js 4.x node-version: "4.9" @@ -161,6 +166,10 @@ jobs: if: steps.list_env.outputs.eslint != '' run: npm run lint + - name: Test types + if: steps.list_env.outputs.typescript != '' + run: npm run test-types + - name: Collect code coverage uses: coverallsapp/github-action@master if: steps.list_env.outputs.nyc != '' diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..fd64cca --- /dev/null +++ b/index.d.ts @@ -0,0 +1,197 @@ +import * as http from 'http'; + +export = Router; + +declare namespace Router { + + interface RouterOptions { + strict?: boolean; + caseSensitive?: boolean; + mergeParams?: boolean; + } + + // Private + interface Layer { + name: string, + handle: RequestHandler | ErrorRequestHandler, + handle_request: RequestHandler, + handle_error: ErrorRequestHandler, + match: (path: string) => boolean, + } + + interface IncomingRequest extends http.IncomingMessage { + originalUrl?: string, + params?: { + [key: string]: any, + }, + } + + interface RoutedRequest extends IncomingRequest { + baseUrl: string, + next?: NextFunction, + route?: IRoute + } + + type RequestParamHandler = (req: RoutedRequest, res: http.ServerResponse, next: NextFunction, value: string, name: string) => void; + + type RouteHandler = (req: RoutedRequest, res: http.ServerResponse, next: NextFunction) => void; + type RequestHandler = (req: IncomingRequest, res: http.ServerResponse, next: NextFunction) => void; + + type NextFunction = (err?: any | "route" | "router") => void; + type Callback = (err?: any) => void; + + type ErrorRequestHandler = (err: any, req: IncomingRequest, res: http.ServerResponse, next: NextFunction) => void; + + type PathParams = string | RegExp | Array; + + type RequestHandlerParams = RouteHandler | ErrorRequestHandler | Array; + + interface IRouterMatcher { + (path: PathParams, ...handlers: RouteHandler[]): T; + (path: PathParams, ...handlers: RequestHandlerParams[]): T; + } + + interface IRouterHandler { + (...handlers: RouteHandler[]): T; + (...handlers: RequestHandlerParams[]): T; + } + + interface IRouter { + /** + * Map the given param placeholder `name`(s) to the given callback(s). + * + * Parameter mapping is used to provide pre-conditions to routes + * which use normalized placeholders. For example a _:user_id_ parameter + * could automatically load a user's information from the database without + * any additional code, + * + * The callback uses the same signature as middleware, the only differencing + * being that the value of the placeholder is passed, in this case the _id_ + * of the user. Once the `next()` function is invoked, just like middleware + * it will continue on to execute the route, or subsequent parameter functions. + * + * app.param('user_id', function(req, res, next, id){ + * User.find(id, function(err, user){ + * if (err) { + * next(err); + * } else if (user) { + * req.user = user; + * next(); + * } else { + * next(new Error('failed to load user')); + * } + * }); + * }); + */ + param(name: string, handler: RequestParamHandler): this; + + /** + * Special-cased "all" method, applying the given route `path`, + * middleware, and callback to _every_ HTTP method. + */ + all: IRouterMatcher; + + use: IRouterHandler & IRouterMatcher; + + // private + // handle: (req: http.IncomingMessage, res: http.ServerResponse, cb: Callback) => void; + + route(prefix: PathParams): IRoute; + // Stack of configured routes. private + // stack: Layer[]; + + // Common HTTP methods + delete: IRouterMatcher; + get: IRouterMatcher; + head: IRouterMatcher; + options: IRouterMatcher; + patch: IRouterMatcher; + post: IRouterMatcher; + put: IRouterMatcher; + + // Exotic HTTP methods + acl: IRouterMatcher; + bind: IRouterMatcher; + checkout: IRouterMatcher; + connect: IRouterMatcher; + copy: IRouterMatcher; + link: IRouterMatcher; + lock: IRouterMatcher; + "m-search": IRouterMatcher; + merge: IRouterMatcher; + mkactivity: IRouterMatcher; + mkcalendar: IRouterMatcher; + mkcol: IRouterMatcher; + move: IRouterMatcher; + notify: IRouterMatcher; + pri: IRouterMatcher; + propfind: IRouterMatcher; + proppatch: IRouterMatcher; + purge: IRouterMatcher; + rebind: IRouterMatcher; + report: IRouterMatcher; + search: IRouterMatcher; + source: IRouterMatcher; + subscribe: IRouterMatcher; + trace: IRouterMatcher; + unbind: IRouterMatcher; + unlink: IRouterMatcher; + unlock: IRouterMatcher; + unsubscribe: IRouterMatcher; + } + + interface IRoute { + path: string; + + // Stack of configured routes. Private. + // stack: Layer[]; + + all: IRouterHandler; + + // Common HTTP methods + delete: IRouterHandler; + get: IRouterHandler; + head: IRouterHandler; + options: IRouterHandler; + patch: IRouterHandler; + post: IRouterHandler; + put: IRouterHandler; + + // Exotic HTTP methods + acl: IRouterHandler; + bind: IRouterHandler; + checkout: IRouterHandler; + connect: IRouterHandler; + copy: IRouterHandler; + link: IRouterHandler; + lock: IRouterHandler; + "m-search": IRouterHandler; + merge: IRouterHandler; + mkactivity: IRouterHandler; + mkcalendar: IRouterHandler; + mkcol: IRouterHandler; + move: IRouterHandler; + notify: IRouterHandler; + pri: IRouterHandler; + propfind: IRouterHandler; + proppatch: IRouterHandler; + purge: IRouterHandler; + rebind: IRouterHandler; + report: IRouterHandler; + search: IRouterHandler; + source: IRouterHandler; + subscribe: IRouterHandler; + trace: IRouterHandler; + unbind: IRouterHandler; + unlink: IRouterHandler; + unlock: IRouterHandler; + unsubscribe: IRouterHandler; + } + + interface RouterConstructor extends IRouter { + new(options?: RouterOptions): IRouter & ((req: http.IncomingMessage, res: http.ServerResponse, cb: Callback) => void); + } + +} + +declare const Router: Router.RouterConstructor; diff --git a/package.json b/package.json index 2495c71..e023a22 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "utils-merge": "1.0.1" }, "devDependencies": { + "@types/node": "^10.0.0", "after": "0.8.2", "eslint": "7.26.0", "eslint-plugin-markdown": "2.1.0", @@ -25,15 +26,18 @@ "mocha": "8.4.0", "nyc": "15.1.0", "safe-buffer": "5.2.1", - "supertest": "6.1.3" + "supertest": "6.1.3", + "typescript": "^4.2.4" }, "files": [ "lib/", "LICENSE", "HISTORY.md", "README.md", + "index.d.ts", "index.js" ], + "types": "index.d.ts", "engines": { "node": ">= 0.8" }, @@ -42,6 +46,7 @@ "test": "mocha --reporter spec --bail --check-leaks test/", "test-ci": "nyc --reporter=lcov --reporter=text npm test", "test-cov": "nyc --reporter=text npm test", + "test-types": "tsc --project tsconfig.json --noEmit", "version": "node scripts/version-history.js && git add HISTORY.md" } } diff --git a/test/type-check.ts b/test/type-check.ts new file mode 100644 index 0000000..3a486bd --- /dev/null +++ b/test/type-check.ts @@ -0,0 +1,66 @@ +import { createServer, IncomingMessage, ServerResponse } from 'http'; +import { + default as Router, + RouterOptions, + IncomingRequest, + RouteHandler, + IRoute, + RoutedRequest +} from '..'; + +const options: RouterOptions = { + strict: false, + caseSensitive: false, + mergeParams: false +}; + +const r = new Router(); +const router = new Router(options); +const routerHandler: RouteHandler = (req, res) => { + res.end('FIN'); +}; + +router.get('/', routerHandler); +router.post('/', routerHandler); +router.delete('/', routerHandler); +router.patch('/', routerHandler); +router.options('/', routerHandler); +router.head('/', routerHandler); +router.unsubscribe('/', routerHandler); + +// param +router.param('user_id', (req, res, next, id) => { + const baseUrl: string = req.baseUrl; + const route: string = req.route?.path || 'na'; + const param = req.params?.user_id; + const val: string = id; + next(); +}); + +// middleware +router.use((req, res, next) => { + next(); +}); + +const api: IRoute = router.route('/api/'); + +router.route('/') +.all((req, res, next) => { + const url: string = req.baseUrl; + next(); +}) +.get((req, res) => { + res.setHeader('x-header', 'value'); + res.end('.'); +}); + + +// test router handling +var server = createServer(function(req: IncomingMessage, res: ServerResponse) { + router(req, res, (err) => { + if (err) { + const e: Error = err; + } + // + }); +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9e23537 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "esModuleInterop": true, + "noImplicitAny": false, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "types": ["node"], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "test/**/*" + ], + "files": [ + "index.d.ts" + ] +}