diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd384f4..e4e0e0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,85 +37,101 @@ 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" npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + npm-rm: typescript @types/node - name: Node.js 5.x node-version: "5.12" npm-i: mocha@5.2.0 nyc@11.9.0 supertest@3.4.2 + npm-rm: typescript @types/node - name: Node.js 6.x node-version: "6.17" npm-i: mocha@6.2.2 nyc@14.1.1 supertest@3.4.2 + npm-rm: typescript @types/node - name: Node.js 7.x node-version: "7.10" npm-i: mocha@6.2.2 nyc@14.1.1 supertest@6.1.6 + npm-rm: typescript @types/node - name: Node.js 8.x node-version: "8.17" - npm-i: mocha@7.2.0 + npm-i: mocha@7.2.0 @types/node@^8.0.0 - name: Node.js 9.x node-version: "9.11" - npm-i: mocha@7.2.0 + npm-i: mocha@7.2.0 @types/node@^9.0.0 - name: Node.js 10.x node-version: "10.24" - npm-i: mocha@8.4.0 + npm-i: mocha@8.4.0 @types/node@^10.0.0 - name: Node.js 11.x node-version: "11.15" - npm-i: mocha@8.4.0 + npm-i: mocha@8.4.0 @types/node@^11.0.0 - name: Node.js 12.x node-version: "12.22" - npm-i: mocha@9.2.2 + npm-i: mocha@9.2.2 @types/node@^12.0.0 - name: Node.js 13.x node-version: "13.14" - npm-i: mocha@9.2.2 + npm-i: mocha@9.2.2 @types/node@^13.0.0 - name: Node.js 14.x - node-version: "14.21" + node-version: "14.21" + npm-i: \@types/node@^14.0.0 - name: Node.js 15.x node-version: "15.14" + npm-i: \@types/node@^15.0.0 - name: Node.js 16.x node-version: "16.19" + npm-i: \@types/node@^16.0.0 - name: Node.js 17.x node-version: "17.9" + npm-i: \@types/node@^17.0.0 - name: Node.js 18.x node-version: "18.14" + npm-i: \@types/node@^18.0.0 - name: Node.js 19.x node-version: "19.7" + npm-i: \@types/node@^18.0.0 # no 19 as yet + steps: - uses: actions/checkout@v3 @@ -182,6 +198,14 @@ jobs: npm test fi + - name: Test type definition + shell: bash + run: | + # testing types requires >= node@8 + if [[ "$(cut -d. -f1 <<< "${{ matrix.node-version }}")" -ge 8 ]]; then + npm run test-types + fi + - name: Lint code if: steps.list_env.outputs.eslint != '' run: npm run lint @@ -194,6 +218,7 @@ jobs: mkdir ./coverage mv "./${{ matrix.name }}" "./coverage/${{ matrix.name }}" fi + - name: Upload code coverage uses: actions/upload-artifact@v3 diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..713bd2f --- /dev/null +++ b/index.d.ts @@ -0,0 +1,122 @@ + +import { OutgoingMessage } from "http"; + +export default Router; + +type HttpMethods = + 'get' | + 'post' | + 'put' | + 'head' | + 'delete' | + 'options' | + 'trace' | + 'copy' | + 'lock' | + 'mkcol' | + 'move' | + 'purge' | + 'propfind' | + 'proppatch' | + 'unlock' | + 'report' | + 'mkactivity' | + 'checkout' | + 'merge' | + 'm-search' | + 'notify' | + 'subscribe' | + 'unsubscribe' | + 'patch' | + 'search' | + 'connect' + +export interface RouterOptions { + strict?: boolean; + caseSensitive?: boolean; + mergeParams?: boolean; +} + +export interface IncomingRequest { + url?: string; + method?: string; + originalUrl?: string; + params?: Record; +} + +interface BaseRoutedRequest extends IncomingRequest { + baseUrl: string; + next?: NextFunction; + route?: IRoute; +} + +export type RoutedRequest = BaseRoutedRequest & { + [key: string]: any; +} + +export interface NextFunction { + (err?: any): void; +} + +type IRoute = Record> & { + path: string; + all: IRouterHandler; +} + +type RequestParamHandler = ( + req: IncomingRequest, + res: OutgoingMessage, + next: NextFunction, + value: string, + name: string +) => void; + +export interface RouteHandler { + (req: RoutedRequest, res: OutgoingMessage, next: NextFunction): void; +} + +export interface RequestHandler { + (req: IncomingRequest, res: OutgoingMessage, next: NextFunction): void; +} + +type ErrorRequestHandler = ( + err: any, + req: IncomingRequest, + res: OutgoingMessage, + 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; +} + +type IRouter = Record> & { + param(name: string, handler: RequestParamHandler): IRouter; + param( + callback: (name: string, matcher: RegExp) => RequestParamHandler + ): IRouter; + all: IRouterMatcher; + use: IRouterHandler & IRouterMatcher; + handle: RequestHandler; + route(prefix: PathParams): IRoute; +} + +interface RouterConstructor { + new (options?: RouterOptions): IRouter & RequestHandler; + (options?: RouterOptions): IRouter & RequestHandler; +} + +declare var Router: RouterConstructor; diff --git a/package.json b/package.json index 5279b12..d494de7 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "utils-merge": "1.0.1" }, "devDependencies": { + "@types/node": "^18.0.0", "after": "0.8.2", "eslint": "8.34.0", "eslint-plugin-markdown": "3.0.0", @@ -25,7 +26,8 @@ "mocha": "10.2.0", "nyc": "15.1.0", "safe-buffer": "5.2.1", - "supertest": "6.3.3" + "supertest": "6.3.3", + "typescript": "^5.0.0" }, "files": [ "lib/", @@ -33,8 +35,12 @@ "HISTORY.md", "README.md", "SECURITY.md", - "index.js" + "index.js", + "index.d.ts" ], + "typesVersions": { + ">=4.0": {"*": ["index.d.ts"]} + }, "engines": { "node": ">= 0.8" }, @@ -43,6 +49,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/types.ts b/test/types.ts new file mode 100644 index 0000000..fc85c83 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,89 @@ +import { createServer, OutgoingMessage } from 'http'; +import Router, { + RouterOptions, + RouteHandler, + NextFunction, + RoutedRequest, + IncomingRequest +} from '..'; + +const options: RouterOptions = { + strict: false, + caseSensitive: false, + mergeParams: false +}; + +// new constructor +new Router().all('/', (req, res, next) => {}) +// direct call +Router().all('/', (req, res, next) => {}) + +const router = new Router(options); +const routerHandler: RouteHandler = (req, res, next) => { + res.setHeader('Content-Type', 'plain/text'); + res.write('Hello') + res.end('world') +}; + +// test verb methods +router.get('/', routerHandler); +router.post('/', routerHandler); +router.delete('/', routerHandler); +router.patch('/', routerHandler); +router.options('/', routerHandler); +router.head('/', routerHandler); +router.bind('/', routerHandler); +router.connect('/', routerHandler); +router.trace('/', routerHandler); +router['m-search']('/', routerHandler); + + +// param +router.param('user_id', (req, res, next, id) => { + type TReq = Expect> + type TRes = Expect> + type TNext = Expect> + type P1 = Expect> +}); + +// middleware +router.use((req, res, next) => { + type TReq = Expect> + type TRes = Expect> + type TNext = Expect> + next(); +}); + +// RoutedRequest is extended with properties without type errors +router.use((req, res, next) => { + req.extendable = 'extendable' + next(); +}); + +router.route('/') +.all((req, res, next) => { + type TReq = Expect> + type TRes = Expect> + type TNext = Expect> + next(); +}) +.get((req, res, next) => { + type TReq = Expect> + type TRes = Expect> + type TNext = Expect> +}); + + +// valid for router from createServer +createServer(function(req, res) { + router(req, res, (err) => {}) + router.handle(req, res, (err) => {}) +}) + + +// Type test helper methods +type Compute = T extends (...args: any[]) => any ? T : { [K in keyof T]: Compute } + +type Equal = (() => T extends Compute ? 1 : 2) extends () => T extends Compute ? 1 : 2 ? true : false + +type Expect = T extends true ? true : never diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9afe7be --- /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" + ] +} \ No newline at end of file