From 6ba05896feb7af80c64a5f890d4c0b95ba01f9bc Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 4 Jun 2023 14:44:33 +0800 Subject: [PATCH] feat: refactor with typescript (#1) closes https://github.com/eggjs/egg-core/issues/264 remove generator support --- .codecov.yml | 4 - .editorconfig | 16 - .eslintrc | 6 + .eslintrc.yml | 1 - .github/FUNDING.yml | 1 - .github/dependabot.yml | 8 - .github/workflows/node.js.yml | 2 +- .github/workflows/release.yml | 15 + .gitignore | 2 + History.md => CHANGELOG.md | 0 Readme.md | 108 +---- __tests__/.eslintrc.yml | 12 - __tests__/application/context.js | 34 -- __tests__/application/currentContext.js | 110 ----- __tests__/application/use.js | 118 ----- __tests__/context/inspect.js | 23 - __tests__/load-with-esm.js | 43 -- __tests__/request/inspect.js | 36 -- index.ts | 3 + lib/application.js | 318 ------------- lib/application.test-d.ts | 10 + package.json | 67 ++- src/application.ts | 285 ++++++++++++ lib/context.js => src/context.ts | 208 ++++----- lib/request.js => src/request.ts | 419 ++++++------------ lib/response.js => src/response.ts | 393 ++++++---------- src/types.d.ts | 13 + test-helpers/context.js | 21 - test/application/context.test.ts | 42 ++ test/application/currentContext.test.ts | 66 +++ .../application/index.test.ts | 30 +- .../application/inspect.test.ts | 10 +- .../application/onerror.test.ts | 45 +- .../application/request.test.ts | 13 +- .../application/respond.test.ts | 90 ++-- .../application/response.test.ts | 25 +- .../application/toJSON.test.ts | 9 +- test/application/use.test.ts | 117 +++++ .../assert.js => test/context/assert.test.ts | 9 +- .../context/cookies.test.ts | 35 +- test/context/inspect.test.ts | 13 + .../context/onerror.test.ts | 86 ++-- .../state.js => test/context/state.test.ts | 9 +- .../throw.js => test/context/throw.test.ts | 11 +- .../toJSON.js => test/context/toJSON.test.ts | 15 +- .../accept.js => test/request/accept.test.ts | 15 +- .../request/accepts.test.ts | 13 +- .../request/acceptsCharsets.test.ts | 11 +- .../request/acceptsEncodings.test.ts | 13 +- .../request/acceptsLanguages.test.ts | 11 +- .../request/charset.test.ts | 11 +- .../fresh.js => test/request/fresh.test.ts | 7 +- .../get.js => test/request/get.test.ts | 7 +- .../header.js => test/request/header.test.ts | 7 +- .../request/headers.test.ts | 7 +- .../host.js => test/request/host.test.ts | 17 +- .../request/hostname.test.ts | 7 +- .../href.js => test/request/href.test.ts | 30 +- .../request/idempotent.test.ts | 11 +- test/request/inspect.test.ts | 33 ++ .../request/ip.js => test/request/ip.test.ts | 25 +- .../ips.js => test/request/ips.test.ts | 13 +- .../request/is.js => test/request/is.test.ts | 15 +- .../length.js => test/request/length.test.ts | 7 +- .../origin.js => test/request/origin.test.ts | 15 +- .../path.js => test/request/path.test.ts | 9 +- .../request/protocol.test.ts | 17 +- .../query.js => test/request/query.test.ts | 12 +- .../request/querystring.test.ts | 11 +- .../search.js => test/request/search.test.ts | 7 +- .../secure.js => test/request/secure.test.ts | 9 +- .../stale.js => test/request/stale.test.ts | 7 +- .../request/subdomains.test.ts | 11 +- .../type.js => test/request/type.test.ts | 7 +- .../request/whatwg-url.test.ts | 7 +- .../append.js => test/response/append.test.ts | 15 +- .../response/attachment.test.ts | 13 +- .../body.js => test/response/body.test.ts | 11 +- .../etag.js => test/response/etag.test.ts | 7 +- .../response/flushHeaders.test.ts | 35 +- .../has.js => test/response/has.test.ts | 7 +- .../header.js => test/response/header.test.ts | 17 +- .../response/headers.test.ts | 9 +- .../response/inspect.test.ts | 15 +- .../is.js => test/response/is.test.ts | 15 +- .../response/last-modified.test.ts | 7 +- .../length.js => test/response/length.test.ts | 9 +- .../response/message.test.ts | 7 +- .../response/redirect.test.ts | 13 +- .../remove.js => test/response/remove.test.ts | 7 +- .../set.js => test/response/set.test.ts | 13 +- .../socket.js => test/response/socket.test.ts | 9 +- .../status.js => test/response/status.test.ts | 35 +- .../type.js => test/response/type.test.ts | 7 +- .../vary.js => test/response/vary.test.ts | 8 +- .../response/writable.test.ts | 19 +- test/test-helpers/context.ts | 35 ++ tsconfig.json | 16 + 98 files changed, 1485 insertions(+), 2117 deletions(-) delete mode 100644 .codecov.yml delete mode 100644 .editorconfig create mode 100644 .eslintrc delete mode 100644 .eslintrc.yml delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/release.yml rename History.md => CHANGELOG.md (100%) delete mode 100644 __tests__/.eslintrc.yml delete mode 100644 __tests__/application/context.js delete mode 100644 __tests__/application/currentContext.js delete mode 100644 __tests__/application/use.js delete mode 100644 __tests__/context/inspect.js delete mode 100644 __tests__/load-with-esm.js delete mode 100644 __tests__/request/inspect.js create mode 100644 index.ts delete mode 100644 lib/application.js create mode 100644 lib/application.test-d.ts create mode 100644 src/application.ts rename lib/context.js => src/context.ts (55%) rename lib/request.js => src/request.ts (67%) rename lib/response.js => src/response.ts (65%) create mode 100644 src/types.d.ts delete mode 100644 test-helpers/context.js create mode 100644 test/application/context.test.ts create mode 100644 test/application/currentContext.test.ts rename __tests__/application/index.js => test/application/index.test.ts (83%) rename __tests__/application/inspect.js => test/application/inspect.test.ts (73%) rename __tests__/application/onerror.js => test/application/onerror.test.ts (50%) rename __tests__/application/request.js => test/application/request.test.ts (75%) rename __tests__/application/respond.js => test/application/respond.test.ts (91%) rename __tests__/application/response.js => test/application/response.test.ts (77%) rename __tests__/application/toJSON.js => test/application/toJSON.test.ts (68%) create mode 100644 test/application/use.test.ts rename __tests__/context/assert.js => test/context/assert.test.ts (69%) rename __tests__/context/cookies.js => test/context/cookies.test.ts (80%) create mode 100644 test/context/inspect.test.ts rename __tests__/context/onerror.js => test/context/onerror.test.ts (79%) rename __tests__/context/state.js => test/context/state.test.ts (71%) rename __tests__/context/throw.js => test/context/throw.test.ts (96%) rename __tests__/context/toJSON.js => test/context/toJSON.test.ts (77%) rename __tests__/request/accept.js => test/request/accept.test.ts (61%) rename __tests__/request/accepts.js => test/request/accepts.test.ts (90%) rename __tests__/request/acceptsCharsets.js => test/request/acceptsCharsets.test.ts (84%) rename __tests__/request/acceptsEncodings.js => test/request/acceptsEncodings.test.ts (77%) rename __tests__/request/acceptsLanguages.js => test/request/acceptsLanguages.test.ts (85%) rename __tests__/request/charset.js => test/request/charset.test.ts (81%) rename __tests__/request/fresh.js => test/request/fresh.test.ts (92%) rename __tests__/request/get.js => test/request/get.test.ts (83%) rename __tests__/request/header.js => test/request/header.test.ts (78%) rename __tests__/request/headers.js => test/request/headers.test.ts (78%) rename __tests__/request/host.js => test/request/host.test.ts (91%) rename __tests__/request/hostname.js => test/request/hostname.test.ts (94%) rename __tests__/request/href.js => test/request/href.test.ts (66%) rename __tests__/request/idempotent.js => test/request/idempotent.test.ts (69%) create mode 100644 test/request/inspect.test.ts rename __tests__/request/ip.js => test/request/ip.test.ts (76%) rename __tests__/request/ips.js => test/request/ips.test.ts (85%) rename __tests__/request/is.js => test/request/is.test.ts (89%) rename __tests__/request/length.js => test/request/length.test.ts (75%) rename __tests__/request/origin.js => test/request/origin.test.ts (65%) rename __tests__/request/path.js => test/request/path.test.ts (87%) rename __tests__/request/protocol.js => test/request/protocol.test.ts (80%) rename __tests__/request/query.js => test/request/query.test.ts (86%) rename __tests__/request/querystring.js => test/request/querystring.test.ts (90%) rename __tests__/request/search.js => test/request/search.test.ts (91%) rename __tests__/request/secure.js => test/request/secure.test.ts (50%) rename __tests__/request/stale.js => test/request/stale.test.ts (75%) rename __tests__/request/subdomains.js => test/request/subdomains.test.ts (70%) rename __tests__/request/type.js => test/request/type.test.ts (76%) rename __tests__/request/whatwg-url.js => test/request/whatwg-url.test.ts (82%) rename __tests__/response/append.js => test/response/append.test.ts (64%) rename __tests__/response/attachment.js => test/response/attachment.test.ts (97%) rename __tests__/response/body.js => test/response/body.test.ts (96%) rename __tests__/response/etag.js => test/response/etag.test.ts (85%) rename __tests__/response/flushHeaders.js => test/response/flushHeaders.test.ts (85%) rename __tests__/response/has.js => test/response/has.test.ts (81%) rename __tests__/response/header.js => test/response/header.test.ts (79%) rename __tests__/response/headers.js => test/response/headers.test.ts (73%) rename __tests__/response/inspect.js => test/response/inspect.test.ts (76%) rename __tests__/response/is.js => test/response/is.test.ts (87%) rename __tests__/response/last-modified.js => test/response/last-modified.test.ts (89%) rename __tests__/response/length.js => test/response/length.test.ts (93%) rename __tests__/response/message.js => test/response/message.test.ts (85%) rename __tests__/response/redirect.js => test/response/redirect.test.ts (95%) rename __tests__/response/remove.js => test/response/remove.test.ts (67%) rename __tests__/response/set.js => test/response/set.test.ts (78%) rename __tests__/response/socket.js => test/response/socket.test.ts (55%) rename __tests__/response/status.js => test/response/status.test.ts (79%) rename __tests__/response/type.js => test/response/type.test.ts (94%) rename __tests__/response/vary.js => test/response/vary.test.ts (85%) rename __tests__/response/writable.js => test/response/writable.test.ts (90%) create mode 100644 test/test-helpers/context.ts create mode 100644 tsconfig.json diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index 3e31ee181..000000000 --- a/.codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -coverage: - parsers: - javascript: - enable_partials: yes diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 7199a70f8..000000000 --- a/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -# editorconfig.org -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false - -[Makefile] -indent_style = tab diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..9bcdb4688 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] +} diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index a1c8ab884..000000000 --- a/.eslintrc.yml +++ /dev/null @@ -1 +0,0 @@ -extends: koa diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ac9e2646e..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -open_collective: koajs diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a17d5bcf7..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 5 - versioning-strategy: increase-if-necessary diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 22967909f..fe77949b8 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -12,5 +12,5 @@ jobs: name: Node.js uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: - os: 'ubuntu-latest' + os: 'ubuntu-latest, macos-latest, windows-latest' version: '16.13.0, 16, 18, 20' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..a4e1158fa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,15 @@ +name: Release + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} + with: + checkTest: false diff --git a/.gitignore b/.gitignore index cd2f2f2cb..e5c9f1457 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ npm-debug.log .idea *.iml dist +lib +!lib/application.test-d.ts 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 e2e380949..b113fcc78 100644 --- a/Readme.md +++ b/Readme.md @@ -2,13 +2,13 @@ @eggjs/koa is forked from [Koa v2.x](https://github.com/koajs/koa/tree/v2.x) for LTS and drop Node.js < 16.13.0 support. -Koa middleware framework for nodejs +Koa middleware framework for nodejs [![NPM version](https://img.shields.io/npm/v/@eggjs/koa.svg?style=flat-square)](https://npmjs.org/package/@eggjs/koa) [![NPM quality](http://npm.packagequality.com/shield/@eggjs/koa.svg?style=flat-square)](http://packagequality.com/#?package=@eggjs/koa) [![NPM download](https://img.shields.io/npm/dm/@eggjs/koa.svg?style=flat-square)](https://npmjs.org/package/@eggjs/koa) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fkoa.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fkoa?ref=badge_shield) -[![Continuous Integration](https://github.com/eggjs/koa/workflows/Continuous%20integration/badge.svg)](https://github.com/eggjs/koa/actions?query=branch%3Amaster) +[![Node.js CI](https://github.com/eggjs/koa/actions/workflows/node.js.yml/badge.svg?branch=master)](https://github.com/eggjs/koa/actions/workflows/node.js.yml) [![Test coverage](https://img.shields.io/codecov/c/github/eggjs/koa.svg?style=flat-square)](https://codecov.io/gh/eggjs/koa) [![Known Vulnerabilities](https://snyk.io/test/npm/@eggjs/koa/badge.svg?style=flat-square)](https://snyk.io/test/npm/@eggjs/koa) @@ -29,7 +29,7 @@ npm install @eggjs/koa ## Hello Koa -```js +```ts const Koa = require('@eggjs/koa'); const app = new Koa(); @@ -56,9 +56,9 @@ Koa is a middleware framework that can take two different kinds of functions as Here is an example of logger middleware with each of the different functions: -### ___async___ functions (node v7.6+) +### ___async___ functions -```js +```ts app.use(async (ctx, next) => { const start = Date.now(); await next(); @@ -69,7 +69,7 @@ app.use(async (ctx, next) => { ### Common function -```js +```ts // Middleware normally takes two parameters (ctx, next), ctx is the context for one request, // next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion. @@ -82,15 +82,6 @@ app.use((ctx, next) => { }); ``` -### Koa v1.x Middleware Signature - -The middleware signature changed between v1.x and v2.x. The older signature is deprecated. - -__Old signature middleware support will be removed in v3__ - -Please see the [Migration Guide](docs/migration.md) for more information on upgrading from v1.x and -using v1.x middleware with v2.x. - ## Context, Request and Response Each middleware receives a Koa `Context` object that encapsulates an incoming @@ -110,7 +101,7 @@ from the node `http` module. Here is an example of checking that a requesting client supports xml. -```js +```ts app.use(async (ctx, next) => { ctx.assert(ctx.request.accepts('xml'), 406); // equivalent to: @@ -132,7 +123,7 @@ accessed as the `res` property on the `Context`. Here is an example using Koa's `Response` object to stream a file as the response body. -```js +```ts app.use(async (ctx, next) => { await next(); ctx.response.type = 'xml'; @@ -186,93 +177,10 @@ See [AUTHORS](AUTHORS). ## Community -- [Badgeboard](https://koajs.github.io/badgeboard) and list of official modules - [Examples](https://github.com/koajs/examples) - [Middleware](https://github.com/koajs/koa/wiki) list - [Wiki](https://github.com/koajs/koa/wiki) -- [Reddit Community](https://www.reddit.com/r/koajs) -- [Mailing list](https://groups.google.com/forum/#!forum/koajs) -- [中文文档 v1.x](https://github.com/guo-yu/koa-guide) - [中文文档 v2.x](https://github.com/demopark/koa-docs-Zh-CN) -- __[#koajs]__ on freenode - -## Job Board - -Looking for a career upgrade? - - - - - -## Backers - -Support us with a monthly donation and help us continue our activities. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -## Sponsors - -Become a sponsor and get your logo on our README on Github with a link to your site. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # License diff --git a/__tests__/.eslintrc.yml b/__tests__/.eslintrc.yml deleted file mode 100644 index d18a96e14..000000000 --- a/__tests__/.eslintrc.yml +++ /dev/null @@ -1,12 +0,0 @@ -env: - jest: true - -rules: - space-before-blocks: [2, {functions: never, keywords: always}] - no-unused-expressions: 0 - node/no-deprecated-api: 'warn' - quote-props: 'warn' - no-prototype-builtins: 'warn' - array-bracket-spacing: 'warn' - object-curly-spacing: 'warn' - dot-notation: 'warn' diff --git a/__tests__/application/context.js b/__tests__/application/context.js deleted file mode 100644 index ec4bc1823..000000000 --- a/__tests__/application/context.js +++ /dev/null @@ -1,34 +0,0 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); - -describe('app.context', () => { - const app1 = new Koa(); - app1.context.msg = 'hello'; - const app2 = new Koa(); - - it('should merge properties', () => { - app1.use((ctx, next) => { - assert.strictEqual(ctx.msg, 'hello'); - ctx.status = 204; - }); - - return request(app1.listen()) - .get('/') - .expect(204); - }); - - it('should not affect the original prototype', () => { - app2.use((ctx, next) => { - assert.strictEqual(ctx.msg, undefined); - ctx.status = 204; - }); - - return request(app2.listen()) - .get('/') - .expect(204); - }); -}); diff --git a/__tests__/application/currentContext.js b/__tests__/application/currentContext.js deleted file mode 100644 index c2aede27b..000000000 --- a/__tests__/application/currentContext.js +++ /dev/null @@ -1,110 +0,0 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); - -describe('app.currentContext', () => { - it('should throw error if AsyncLocalStorage not support', () => { - if (require('async_hooks').AsyncLocalStorage) return; - assert.throws(() => new Koa({ asyncLocalStorage: true }), - /Requires node 12\.17\.0 or higher to enable asyncLocalStorage/); - }); - - it('should get currentContext return context when asyncLocalStorage enable', async() => { - if (!require('async_hooks').AsyncLocalStorage) return; - - const app = new Koa({ asyncLocalStorage: true }); - - app.use(async ctx => { - assert(ctx === app.currentContext); - await new Promise(resolve => { - setTimeout(() => { - assert(ctx === app.currentContext); - resolve(); - }, 1); - }); - await new Promise(resolve => { - assert(ctx === app.currentContext); - setImmediate(() => { - assert(ctx === app.currentContext); - resolve(); - }); - }); - assert(ctx === app.currentContext); - app.currentContext.body = 'ok'; - }); - - const requestServer = async() => { - assert(app.currentContext === undefined); - await request(app.callback()).get('/').expect('ok'); - assert(app.currentContext === undefined); - }; - - await Promise.all([ - requestServer(), - requestServer(), - requestServer(), - requestServer(), - requestServer() - ]); - }); - - it('should get currentContext return undefined when asyncLocalStorage disable', async() => { - const app = new Koa(); - - app.use(async ctx => { - assert(app.currentContext === undefined); - ctx.body = 'ok'; - }); - - await request(app.callback()).get('/').expect('ok'); - }); - - it('should get currentContext return context in error handler when asyncLocalStorage enable', async() => { - const app = new Koa({ asyncLocalStorage: true }); - - app.use(async() => { - throw new Error('error message'); - }); - - const handleError = new Promise((resolve, reject) => { - app.on('error', (err, ctx) => { - try { - assert.strictEqual(err.message, 'error message'); - assert.strictEqual(app.currentContext, ctx); - resolve(); - } catch (e) { - reject(e); - } - }); - }); - - await request(app.callback()).get('/').expect('Internal Server Error'); - await handleError; - }); - - it('should get currentContext return undefined in error handler when asyncLocalStorage disable', async() => { - const app = new Koa(); - - app.use(async() => { - throw new Error('error message'); - }); - - const handleError = new Promise((resolve, reject) => { - app.on('error', (err, ctx) => { - try { - assert.strictEqual(err.message, 'error message'); - assert.strictEqual(app.currentContext, undefined); - resolve(); - } catch (e) { - reject(e); - } - }); - }); - - await request(app.callback()).get('/').expect('Internal Server Error'); - await handleError; - }); -}); diff --git a/__tests__/application/use.js b/__tests__/application/use.js deleted file mode 100644 index c333d8c9f..000000000 --- a/__tests__/application/use.js +++ /dev/null @@ -1,118 +0,0 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); - -describe('app.use(fn)', () => { - it('should compose middleware', async() => { - const app = new Koa(); - const calls = []; - - app.use((ctx, next) => { - calls.push(1); - return next().then(() => { - calls.push(6); - }); - }); - - app.use((ctx, next) => { - calls.push(2); - return next().then(() => { - calls.push(5); - }); - }); - - app.use((ctx, next) => { - calls.push(3); - return next().then(() => { - calls.push(4); - }); - }); - - const server = app.listen(); - - await request(server) - .get('/') - .expect(404); - - assert.deepStrictEqual(calls, [1, 2, 3, 4, 5, 6]); - }); - - it('should compose mixed middleware', async() => { - process.once('deprecation', () => {}); // silence deprecation message - const app = new Koa(); - const calls = []; - - app.use((ctx, next) => { - calls.push(1); - return next().then(() => { - calls.push(6); - }); - }); - - app.use(function * (next){ - calls.push(2); - yield next; - calls.push(5); - }); - - app.use((ctx, next) => { - calls.push(3); - return next().then(() => { - calls.push(4); - }); - }); - - const server = app.listen(); - - await request(server) - .get('/') - .expect(404); - - assert.deepStrictEqual(calls, [1, 2, 3, 4, 5, 6]); - }); - - // https://github.com/koajs/koa/pull/530#issuecomment-148138051 - it('should catch thrown errors in non-async functions', () => { - const app = new Koa(); - - app.use(ctx => ctx.throw('Not Found', 404)); - - return request(app.callback()) - .get('/') - .expect(404); - }); - - it('should accept both generator and function middleware', () => { - process.once('deprecation', () => {}); // silence deprecation message - const app = new Koa(); - - app.use((ctx, next) => next()); - app.use(function * (next){ this.body = 'generator'; }); - - return request(app.callback()) - .get('/') - .expect(200) - .expect('generator'); - }); - - it('should throw error for non-function', () => { - const app = new Koa(); - - [null, undefined, 0, false, 'not a function'].forEach(v => { - assert.throws(() => app.use(v), /middleware must be a function!/); - }); - }); - - it('should output deprecation message for generator functions', done => { - process.once('deprecation', message => { - assert(/Support for generators will be removed/.test(message)); - done(); - }); - - const app = new Koa(); - app.use(function * (){}); - }); -}); diff --git a/__tests__/context/inspect.js b/__tests__/context/inspect.js deleted file mode 100644 index 22945aa6c..000000000 --- a/__tests__/context/inspect.js +++ /dev/null @@ -1,23 +0,0 @@ - -'use strict'; - -const prototype = require('../../lib/context'); -const assert = require('assert'); -const util = require('util'); -const context = require('../../test-helpers/context'); - -describe('ctx.inspect()', () => { - it('should return a json representation', () => { - const ctx = context(); - const toJSON = ctx.toJSON(ctx); - - assert.deepStrictEqual(toJSON, ctx.inspect()); - assert.deepStrictEqual(util.inspect(toJSON), util.inspect(ctx)); - }); - - // console.log(require.cache) will call prototype.inspect() - it('should not crash when called on the prototype', () => { - assert.deepStrictEqual(prototype, prototype.inspect()); - assert.deepStrictEqual(util.inspect(prototype.inspect()), util.inspect(prototype)); - }); -}); diff --git a/__tests__/load-with-esm.js b/__tests__/load-with-esm.js deleted file mode 100644 index fc451eda5..000000000 --- a/__tests__/load-with-esm.js +++ /dev/null @@ -1,43 +0,0 @@ -const assert = require('assert'); - -let importESM = () => {}; - -describe.skip('Load with esm', () => { - beforeAll(() => { - // eslint-disable-next-line no-eval - importESM = eval('(specifier) => import(specifier)'); - }); - - it('should default export koa', async() => { - const exported = await importESM('koa'); - const required = require('../'); - assert.strictEqual(exported.default, required); - }); - - it('should match exports own property names', async() => { - const exported = new Set(Object.getOwnPropertyNames(await importESM('koa'))); - const required = new Set(Object.getOwnPropertyNames(require('../'))); - - // Remove constructor properties + default export. - for (const k of ['prototype', 'length', 'name']) { - required.delete(k); - } - - // Commented out to "fix" CommonJS, ESM, bundling issue. - // @see https://github.com/koajs/koa/issues/1513 - // exported.delete('default'); - - assert.strictEqual(exported.size, required.size); - assert.strictEqual([...exported].every(property => required.has(property)), true); - }); - - it('CommonJS exports default property', async() => { - const required = require('../'); - assert.strictEqual(required.hasOwnProperty('default'), true); - }); - - it('CommonJS exports default property referencing self', async() => { - const required = require('../'); - assert.strictEqual(required.default, required); - }); -}); diff --git a/__tests__/request/inspect.js b/__tests__/request/inspect.js deleted file mode 100644 index 89a00ff99..000000000 --- a/__tests__/request/inspect.js +++ /dev/null @@ -1,36 +0,0 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); -const util = require('util'); - -describe('req.inspect()', () => { - describe('with no request.req present', () => { - it('should return null', () => { - const req = request(); - req.method = 'GET'; - delete req.req; - assert(undefined === req.inspect()); - assert('undefined' === util.inspect(req)); - }); - }); - - it('should return a json representation', () => { - const req = request(); - req.method = 'GET'; - req.url = 'example.com'; - req.header.host = 'example.com'; - - const expected = { - method: 'GET', - url: 'example.com', - header: { - host: 'example.com' - } - }; - - assert.deepStrictEqual(req.inspect(), expected); - assert.deepStrictEqual(util.inspect(req), util.inspect(expected)); - }); -}); diff --git a/index.ts b/index.ts new file mode 100644 index 000000000..a567fe7d8 --- /dev/null +++ b/index.ts @@ -0,0 +1,3 @@ +import Application from './src/application'; + +export default Application; diff --git a/lib/application.js b/lib/application.js deleted file mode 100644 index 5ebe179a2..000000000 --- a/lib/application.js +++ /dev/null @@ -1,318 +0,0 @@ - -'use strict'; - -/** - * Module dependencies. - */ - -const isGeneratorFunction = require('is-generator-function'); -const debug = require('debug')('koa:application'); -const onFinished = require('on-finished'); -const assert = require('assert'); -const response = require('./response'); -const compose = require('koa-compose'); -const context = require('./context'); -const request = require('./request'); -const statuses = require('statuses'); -const Emitter = require('events'); -const util = require('util'); -const Stream = require('stream'); -const http = require('http'); -const only = require('only'); -const convert = require('koa-convert'); -const deprecate = require('depd')('koa'); -const { HttpError } = require('http-errors'); - -/** - * Expose `Application` class. - * Inherits from `Emitter.prototype`. - */ - -module.exports = class Application extends Emitter { - /** - * Initialize a new `Application`. - * - * @api public - */ - - /** - * - * @param {object} [options] Application options - * @param {string} [options.env='development'] Environment - * @param {string[]} [options.keys] Signed cookie keys - * @param {boolean} [options.proxy] Trust proxy headers - * @param {number} [options.subdomainOffset] Subdomain offset - * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For - * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity) - * - */ - - constructor(options) { - super(); - options = options || {}; - this.proxy = options.proxy || false; - this.subdomainOffset = options.subdomainOffset || 2; - this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; - this.maxIpsCount = options.maxIpsCount || 0; - this.env = options.env || process.env.NODE_ENV || 'development'; - if (options.keys) this.keys = options.keys; - this.middleware = []; - this.context = Object.create(context); - this.request = Object.create(request); - this.response = Object.create(response); - // util.inspect.custom support for node 6+ - /* istanbul ignore else */ - if (util.inspect.custom) { - this[util.inspect.custom] = this.inspect; - } - if (options.asyncLocalStorage) { - const { AsyncLocalStorage } = require('async_hooks'); - assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage'); - this.ctxStorage = new AsyncLocalStorage(); - } - } - - /** - * Shorthand for: - * - * http.createServer(app.callback()).listen(...) - * - * @param {Mixed} ... - * @return {Server} - * @api public - */ - - listen(...args) { - debug('listen'); - const server = http.createServer(this.callback()); - return server.listen(...args); - } - - /** - * Return JSON representation. - * We only bother showing settings. - * - * @return {Object} - * @api public - */ - - toJSON() { - return only(this, [ - 'subdomainOffset', - 'proxy', - 'env' - ]); - } - - /** - * Inspect implementation. - * - * @return {Object} - * @api public - */ - - inspect() { - return this.toJSON(); - } - - /** - * Use the given middleware `fn`. - * - * Old-style middleware will be converted. - * - * @param {Function} fn - * @return {Application} self - * @api public - */ - - use(fn) { - if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); - if (isGeneratorFunction(fn)) { - deprecate('Support for generators will be removed in v3. ' + - 'See the documentation for examples of how to convert old middleware ' + - 'https://github.com/koajs/koa/blob/master/docs/migration.md'); - fn = convert(fn); - } - debug('use %s', fn._name || fn.name || '-'); - this.middleware.push(fn); - return this; - } - - /** - * Return a request handler callback - * for node's native http server. - * - * @return {Function} - * @api public - */ - - callback() { - const fn = compose(this.middleware); - - if (!this.listenerCount('error')) this.on('error', this.onerror); - - const handleRequest = (req, res) => { - const ctx = this.createContext(req, res); - if (!this.ctxStorage) { - return this.handleRequest(ctx, fn); - } - return this.ctxStorage.run(ctx, async() => { - return await this.handleRequest(ctx, fn); - }); - }; - - return handleRequest; - } - - /** - * return currnect contenxt from async local storage - */ - get currentContext() { - if (this.ctxStorage) return this.ctxStorage.getStore(); - } - - /** - * Handle request in callback. - * - * @api private - */ - - handleRequest(ctx, fnMiddleware) { - const res = ctx.res; - res.statusCode = 404; - const onerror = err => ctx.onerror(err); - const handleResponse = () => respond(ctx); - onFinished(res, onerror); - return fnMiddleware(ctx).then(handleResponse).catch(onerror); - } - - /** - * Initialize a new context. - * - * @api private - */ - - createContext(req, res) { - const context = Object.create(this.context); - const request = context.request = Object.create(this.request); - const response = context.response = Object.create(this.response); - context.app = request.app = response.app = this; - context.req = request.req = response.req = req; - context.res = request.res = response.res = res; - request.ctx = response.ctx = context; - request.response = response; - response.request = request; - context.originalUrl = request.originalUrl = req.url; - context.state = {}; - return context; - } - - /** - * Default error handler. - * - * @param {Error} err - * @api private - */ - - onerror(err) { - // When dealing with cross-globals a normal `instanceof` check doesn't work properly. - // See https://github.com/koajs/koa/issues/1466 - // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549. - const isNativeError = - Object.prototype.toString.call(err) === '[object Error]' || - err instanceof Error; - if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err)); - - if (404 === err.status || err.expose) return; - if (this.silent) return; - - const msg = err.stack || err.toString(); - console.error(`\n${msg.replace(/^/gm, ' ')}\n`); - } - - /** - * Help TS users comply to CommonJS, ESM, bundler mismatch. - * @see https://github.com/koajs/koa/issues/1513 - */ - - static get default() { - return Application; - } - - createAsyncCtxStorageMiddleware() { - const app = this; - return async function asyncCtxStorage(ctx, next) { - await app.ctxStorage.run(ctx, async() => { - return await next(); - }); - }; - } -}; - -/** - * Response helper. - */ - -function respond(ctx) { - // allow bypassing koa - if (false === ctx.respond) return; - - if (!ctx.writable) return; - - const res = ctx.res; - let body = ctx.body; - const code = ctx.status; - - // ignore body - if (statuses.empty[code]) { - // strip headers - ctx.body = null; - return res.end(); - } - - if ('HEAD' === ctx.method) { - if (!res.headersSent && !ctx.response.has('Content-Length')) { - const { length } = ctx.response; - if (Number.isInteger(length)) ctx.length = length; - } - return res.end(); - } - - // status body - if (null == body) { - if (ctx.response._explicitNullBody) { - ctx.response.remove('Content-Type'); - ctx.response.remove('Transfer-Encoding'); - return res.end(); - } - if (ctx.req.httpVersionMajor >= 2) { - body = String(code); - } else { - body = ctx.message || String(code); - } - if (!res.headersSent) { - ctx.type = 'text'; - ctx.length = Buffer.byteLength(body); - } - return res.end(body); - } - - // responses - if (Buffer.isBuffer(body)) return res.end(body); - if ('string' === typeof body) return res.end(body); - if (body instanceof Stream) return body.pipe(res); - - // body: json - body = JSON.stringify(body); - if (!res.headersSent) { - ctx.length = Buffer.byteLength(body); - } - res.end(body); -} - -/** - * Make HttpError available to consumers of the library so that consumers don't - * have a direct dependency upon `http-errors` - */ - -module.exports.HttpError = HttpError; diff --git a/lib/application.test-d.ts b/lib/application.test-d.ts new file mode 100644 index 000000000..0c33ee446 --- /dev/null +++ b/lib/application.test-d.ts @@ -0,0 +1,10 @@ +import { expectType } from 'tsd'; +import { Context } from '../src/application'; +import Application from '../src/application'; + +const ctx = {} as Context; +expectType(ctx.ip); + +const app = {} as Application; +expectType(app.env); +expectType(app.ctxStorage.getStore()); diff --git a/package.json b/package.json index 147ff2ec6..dc0e5def8 100644 --- a/package.json +++ b/package.json @@ -3,37 +3,25 @@ "version": "2.14.2", "description": "Koa web app framework", "main": "lib/application.js", + "types": "lib/application.d.ts", "files": [ - "dist", - "lib" + "lib/*.d.ts", + "lib/*.js" ], - "exports": { - ".": { - "require": "./lib/application.js", - "import": "./dist/koa.mjs" - }, - "./lib/request": "./lib/request.js", - "./lib/request.js": "./lib/request.js", - "./lib/response": "./lib/response.js", - "./lib/response.js": "./lib/response.js", - "./lib/application": "./lib/application.js", - "./lib/application.js": "./lib/application.js", - "./lib/context": "./lib/context.js", - "./lib/context.js": "./lib/context.js", - "./*": "./*.js", - "./*.js": "./*.js", - "./package": "./package.json", - "./package.json": "./package.json" - }, "scripts": { - "test": "jest --forceExit", - "ci": "npm test -- --coverage --maxWorkers 2", - "lint": "eslint --ignore-path .gitignore .", + "test": "egg-bin test", + "ci": "egg-bin cov && npm run tsd", + "lint": "eslint src test", + "tsd": "npm run tsc && tsd", "authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS", - "build": "gen-esm-wrapper . ./dist/koa.mjs", - "prepare": "npm run build" + "clean": "tsc -b --clean", + "tsc": "tsc", + "prepublishOnly": "npm run tsc" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/koa.git" }, - "repository": "eggjs/koa", "keywords": [ "web", "app", @@ -50,9 +38,7 @@ "content-disposition": "~0.5.2", "content-type": "^1.0.4", "cookies": "~0.8.0", - "debug": "^4.3.2", "delegates": "^1.0.0", - "depd": "^2.0.0", "destroy": "^1.0.4", "encodeurl": "^1.0.2", "escape-html": "^1.0.3", @@ -61,7 +47,6 @@ "http-errors": "^1.6.3", "is-generator-function": "^1.0.7", "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", "on-finished": "^2.3.0", "only": "~0.0.2", "parseurl": "^1.3.2", @@ -70,23 +55,21 @@ "vary": "^1.1.2" }, "devDependencies": { - "eslint": "^7.32.0", - "eslint-config-koa": "^2.0.0", - "eslint-config-standard": "^16.0.3", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^5.1.0", - "eslint-plugin-standard": "^5.0.0", - "gen-esm-wrapper": "^1.0.6", - "jest": "^27.0.6", - "supertest": "^3.1.0" + "@eggjs/tsconfig": "^1.3.3", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.5", + "egg-bin": "^6.4.0", + "eslint": "^8.41.0", + "eslint-config-egg": "^12.2.1", + "mm": "^3.3.0", + "supertest": "^3.1.0", + "ts-node": "^10.9.1", + "tsd": "^0.28.1", + "typescript": "^5.0.4" }, "engines": { "node": ">= 16.13.0" }, - "jest": { - "testEnvironment": "node" - }, "publishConfig": { "access": "public" } diff --git a/src/application.ts b/src/application.ts new file mode 100644 index 000000000..df10888f7 --- /dev/null +++ b/src/application.ts @@ -0,0 +1,285 @@ +import { debuglog } from 'node:util'; +import Emitter from 'node:events'; +import util from 'node:util'; +import Stream from 'node:stream'; +import http from 'node:http'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import isGeneratorFunction from 'is-generator-function'; +import onFinished from 'on-finished'; +import statuses from 'statuses'; +import compose from 'koa-compose'; +import only from 'only'; +import { HttpError } from 'http-errors'; +import Context from './context'; +import Request from './request'; +import Response from './response'; +import type { ContextDelegation } from './context'; +import type { CustomError, ProtoImplClass, AnyProto } from './types'; + +const debug = debuglog('koa:application'); + +export type Next = () => Promise; +export type MiddlewareFunc = (ctx: ContextDelegation, next: Next) => Promise | void; + +export type { ContextDelegation as Context } from './context'; +export type { CustomError, ProtoImplClass } from './types'; + +/** + * Expose `Application` class. + * Inherits from `Emitter.prototype`. + */ +export default class Application extends Emitter { + /** + * Make HttpError available to consumers of the library so that consumers don't + * have a direct dependency upon `http-errors` + */ + static HttpError = HttpError; + + proxy: boolean; + subdomainOffset: number; + proxyIpHeader: string; + maxIpsCount: number; + env: string; + keys?: string[]; + middleware: MiddlewareFunc[]; + ctxStorage: AsyncLocalStorage; + silent: boolean; + ContextClass: ProtoImplClass; + context: AnyProto; + RequestClass: ProtoImplClass; + request: AnyProto; + ResponseClass: ProtoImplClass; + response: AnyProto; + + /** + * Initialize a new `Application`. + * + * @param {object} [options] Application options + * @param {string} [options.env='development'] Environment + * @param {string[]} [options.keys] Signed cookie keys + * @param {boolean} [options.proxy] Trust proxy headers + * @param {number} [options.subdomainOffset] Subdomain offset + * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For + * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity) + */ + + constructor(options?: { + proxy?: boolean; + subdomainOffset?: number; + proxyIpHeader?: string; + maxIpsCount?: number; + env?: string; + keys?: string[]; + }) { + super(); + options = options || {}; + this.proxy = options.proxy || false; + this.subdomainOffset = options.subdomainOffset || 2; + this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; + this.maxIpsCount = options.maxIpsCount || 0; + this.env = options.env || process.env.NODE_ENV || 'development'; + if (options.keys) this.keys = options.keys; + this.middleware = []; + this.ctxStorage = new AsyncLocalStorage(); + this.silent = false; + this.ContextClass = class ApplicationContext extends Context {}; + this.context = this.ContextClass.prototype; + this.RequestClass = class ApplicationRequest extends Request {}; + this.request = this.RequestClass.prototype; + this.ResponseClass = class ApplicationResponse extends Response {}; + this.response = this.ResponseClass.prototype; + } + + /** + * Shorthand for: + * + * http.createServer(app.callback()).listen(...) + */ + listen(...args: any[]) { + debug('listen'); + const server = http.createServer(this.callback()); + return server.listen(...args); + } + + /** + * Return JSON representation. + * We only bother showing settings. + */ + toJSON() { + return only(this, [ + 'subdomainOffset', + 'proxy', + 'env', + ]); + } + + /** + * Inspect implementation. + */ + inspect() { + return this.toJSON(); + } + + [util.inspect.custom]() { + return this.inspect(); + } + + /** + * Use the given middleware `fn`. + * + * Old-style middleware will be converted. + */ + use(fn: MiddlewareFunc) { + if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); + if (isGeneratorFunction(fn)) { + throw new TypeError('Support for generators was removed. ' + + 'See the documentation for examples of how to convert old middleware ' + + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); + } + debug('use %s #%d', (fn as any)._name || fn.name || '-', this.middleware.length); + this.middleware.push(fn); + return this; + } + + /** + * Return a request handler callback + * for node's native http server. + */ + callback() { + const fn = compose(this.middleware); + + if (!this.listenerCount('error')) { + this.on('error', this.onerror.bind(this)); + } + + const handleRequest = (req: IncomingMessage, res: ServerResponse) => { + const ctx = this.createContext(req, res); + return this.ctxStorage.run(ctx, async () => { + return await this.#handleRequest(ctx, fn); + }); + }; + + return handleRequest; + } + + /** + * return currnect contenxt from async local storage + */ + get currentContext() { + if (this.ctxStorage) return this.ctxStorage.getStore(); + } + + /** + * Handle request in callback. + * @private + */ + async #handleRequest(ctx: ContextDelegation, fnMiddleware: (ctx: ContextDelegation) => Promise) { + const res = ctx.res; + res.statusCode = 404; + const onerror = (err: Error) => ctx.onerror(err); + onFinished(res, onerror); + try { + await fnMiddleware(ctx); + return this._respond(ctx); + } catch (err) { + return onerror(err); + } + } + + /** + * Initialize a new context. + * @private + */ + protected createContext(req: IncomingMessage, res: ServerResponse) { + const context = new this.ContextClass(this, req, res); + return context as ContextDelegation; + } + + /** + * Default error handler. + * @private + */ + protected onerror(err: CustomError) { + // When dealing with cross-globals a normal `instanceof` check doesn't work properly. + // See https://github.com/koajs/koa/issues/1466 + // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549. + const isNativeError = err instanceof Error || + Object.prototype.toString.call(err) === '[object Error]'; + if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err)); + + if (err.status === 404 || err.expose) return; + if (this.silent) return; + + const msg = err.stack || err.toString(); + console.error(`\n${msg.replace(/^/gm, ' ')}\n`); + } + + createAsyncCtxStorageMiddleware() { + return async (ctx: ContextDelegation, next: Next) => { + await this.ctxStorage.run(ctx, async () => { + return await next(); + }); + }; + } + + /** + * Response helper. + */ + protected _respond(ctx: ContextDelegation) { + // allow bypassing koa + if (ctx.respond === false) return; + + if (!ctx.writable) return; + + const res = ctx.res; + let body = ctx.body; + const code = ctx.status; + + // ignore body + if (statuses.empty[code]) { + // strip headers + ctx.body = null; + return res.end(); + } + + if (ctx.method === 'HEAD') { + if (!res.headersSent && !ctx.response.has('Content-Length')) { + const { length } = ctx.response; + if (Number.isInteger(length)) ctx.length = length; + } + return res.end(); + } + + // status body + if (body == null) { + if (ctx.response._explicitNullBody) { + ctx.response.remove('Content-Type'); + ctx.response.remove('Transfer-Encoding'); + return res.end(); + } + if (ctx.req.httpVersionMajor >= 2) { + body = String(code); + } else { + body = ctx.message || String(code); + } + if (!res.headersSent) { + ctx.type = 'text'; + ctx.length = Buffer.byteLength(body); + } + return res.end(body); + } + + // responses + if (Buffer.isBuffer(body)) return res.end(body); + if (typeof body === 'string') return res.end(body); + if (body instanceof Stream) return body.pipe(res); + + // body: json + body = JSON.stringify(body); + if (!res.headersSent) { + ctx.length = Buffer.byteLength(body); + } + res.end(body); + } +} diff --git a/lib/context.js b/src/context.ts similarity index 55% rename from lib/context.js rename to src/context.ts index f6c0f111b..dfbd8127b 100644 --- a/lib/context.js +++ b/src/context.ts @@ -1,37 +1,51 @@ - -'use strict'; - -/** - * Module dependencies. - */ - -const util = require('util'); -const createError = require('http-errors'); -const httpAssert = require('http-assert'); -const delegate = require('delegates'); -const statuses = require('statuses'); -const Cookies = require('cookies'); - -const COOKIES = Symbol('context#cookies'); - -/** - * Context prototype. - */ - -const proto = module.exports = { +import util from 'node:util'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import createError from 'http-errors'; +import httpAssert from 'http-assert'; +import delegate from 'delegates'; +import statuses from 'statuses'; +import Cookies from 'cookies'; +import type Application from './application'; +import type Request from './request'; +import type Response from './response'; +import type { CustomError, AnyProto } from './types'; + +export default class Context { + app: Application; + req: IncomingMessage; + res: ServerResponse; + request: Request & AnyProto; + response: Response & AnyProto; + state: Record; + originalUrl: string; + respond?: boolean; + + constructor(app: Application, req: IncomingMessage, res: ServerResponse) { + this.app = app; + this.req = req; + this.res = res; + this.state = {}; + this.request = new app.RequestClass(app, this, req, res); + this.response = new app.ResponseClass(app, this as any, req, res); + this.request.response = this.response; + this.response.request = this.request; + this.originalUrl = req.url!; + } /** * util.inspect() implementation, which * just returns the JSON output. - * - * @return {Object} - * @api public */ - inspect() { - if (this === proto) return this; return this.toJSON(); - }, + } + + /** + * Custom inspection implementation for newer Node.js versions. + */ + [util.inspect.custom]() { + return this.inspect(); + } /** * Return JSON representation. @@ -40,9 +54,6 @@ const proto = module.exports = { * object, as iteration will otherwise fail due * to the getters and cause utilities such as * clone() to fail. - * - * @return {Object} - * @api public */ toJSON() { @@ -53,9 +64,9 @@ const proto = module.exports = { originalUrl: this.originalUrl, req: '', res: '', - socket: '' + socket: '', }; - }, + } /** * Similar to .throw(), adds assertion. @@ -67,10 +78,12 @@ const proto = module.exports = { * @param {Mixed} test * @param {Number} status * @param {String} message - * @api public + * @public */ - assert: httpAssert, + assert(...args: any[]) { + return httpAssert(...args); + } /** * Throw an error with `status` (default 500) and @@ -90,37 +103,32 @@ const proto = module.exports = { * @param {String|Number|Error} err, msg or status * @param {String|Number|Error} [err, msg or status] * @param {Object} [props] - * @api public */ - throw(...args) { + throw(...args: any[]) { throw createError(...args); - }, + } /** * Default error handling. - * - * @param {Error} err - * @api private + * @private */ - - onerror(err) { + onerror(err: CustomError) { // don't do anything if there is no error. // this allows you to pass `this.onerror` // to node-style callbacks. - if (null == err) return; + if (err === null || err === undefined) return; // When dealing with cross-globals a normal `instanceof` check doesn't work properly. // See https://github.com/koajs/koa/issues/1466 // We can probably remove it once jest fixes https://github.com/facebook/jest/issues/2549. - const isNativeError = - Object.prototype.toString.call(err) === '[object Error]' || - err instanceof Error; + const isNativeError = err instanceof Error || + Object.prototype.toString.call(err) === '[object Error]'; if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err)); let headerSent = false; - if (this.headerSent || !this.writable) { - headerSent = err.headerSent = true; + if (this.response.headerSent || !this.response.writable) { + headerSent = (err as any).headerSent = true; } // delegate @@ -136,90 +144,51 @@ const proto = module.exports = { const { res } = this; // first unset all headers - /* istanbul ignore else */ - if (typeof res.getHeaderNames === 'function') { - res.getHeaderNames().forEach(name => res.removeHeader(name)); - } else { - res._headers = {}; // Node < 7.7 - } + res.getHeaderNames().forEach(name => res.removeHeader(name)); // then set those specified - this.set(err.headers); + if (err.headers) this.response.set(err.headers); // force text/plain - this.type = 'text'; + this.response.type = 'text'; let statusCode = err.status || err.statusCode; // ENOENT support - if ('ENOENT' === err.code) statusCode = 404; + if (err.code === 'ENOENT') statusCode = 404; // default to 500 - if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500; + if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500; // respond const code = statuses[statusCode]; const msg = err.expose ? err.message : code; - this.status = err.status = statusCode; - this.length = Buffer.byteLength(msg); + this.response.status = err.status = statusCode; + this.response.length = Buffer.byteLength(msg); res.end(msg); - }, + } + #cookies: Cookies; get cookies() { - if (!this[COOKIES]) { - this[COOKIES] = new Cookies(this.req, this.res, { + if (!this.#cookies) { + this.#cookies = new Cookies(this.req, this.res, { keys: this.app.keys, - secure: this.request.secure + secure: this.request.secure, }); } - return this[COOKIES]; - }, - - set cookies(_cookies) { - this[COOKIES] = _cookies; + return this.#cookies; } -}; -/** - * Custom inspection implementation for newer Node.js versions. - * - * @return {Object} - * @api public - */ - -/* istanbul ignore else */ -if (util.inspect.custom) { - module.exports[util.inspect.custom] = module.exports.inspect; + set cookies(cookies: Cookies) { + this.#cookies = cookies; + } } -/** - * Response delegation. - */ - -delegate(proto, 'response') - .method('attachment') - .method('redirect') - .method('remove') - .method('vary') - .method('has') - .method('set') - .method('append') - .method('flushHeaders') - .access('status') - .access('message') - .access('body') - .access('length') - .access('type') - .access('lastModified') - .access('etag') - .getter('headerSent') - .getter('writable'); - /** * Request delegation. */ -delegate(proto, 'request') +delegate(Context.prototype, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') @@ -249,3 +218,34 @@ delegate(proto, 'request') .getter('fresh') .getter('ips') .getter('ip'); + +/** + * Response delegation. + */ + +delegate(Context.prototype, 'response') + .method('attachment') + .method('redirect') + .method('remove') + .method('vary') + .method('has') + .method('set') + .method('append') + .method('flushHeaders') + .access('status') + .access('message') + .access('body') + .access('length') + .access('type') + .access('lastModified') + .access('etag') + .getter('headerSent') + .getter('writable'); + +export type ContextDelegation = Context & Pick +& Pick +& AnyProto; diff --git a/lib/request.js b/src/request.ts similarity index 67% rename from lib/request.js rename to src/request.ts index e62afd606..f4e7be2ac 100644 --- a/lib/request.js +++ b/src/request.ts @@ -1,155 +1,128 @@ - -'use strict'; - -/** - * Module dependencies. - */ - -const URL = require('url').URL; -const net = require('net'); -const accepts = require('accepts'); -const contentType = require('content-type'); -const stringify = require('url').format; -const parse = require('parseurl'); -const qs = require('querystring'); -const typeis = require('type-is'); -const fresh = require('fresh'); -const only = require('only'); -const util = require('util'); - -const IP = Symbol('context#ip'); - -/** - * Prototype. - */ - -module.exports = { +import net from 'node:net'; +import type { Socket } from 'node:net'; +import { format as stringify } from 'node:url'; +import qs from 'node:querystring'; +import util from 'node:util'; +import type { ParsedUrlQuery } from 'node:querystring'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import accepts from 'accepts'; +import contentType from 'content-type'; +import parse from 'parseurl'; +import typeis from 'type-is'; +import fresh from 'fresh'; +import only from 'only'; +import type Application from './application'; +import type Context from './context'; +import type Response from './response'; + +export default class Request { + app: Application; + req: IncomingMessage; + res: ServerResponse; + ctx: Context; + response: Response; + originalUrl: string; + + constructor(app: Application, ctx: Context, req: IncomingMessage, res: ServerResponse) { + this.app = app; + this.req = req; + this.res = res; + this.ctx = ctx; + this.originalUrl = req.url!; + } /** * Return request header. - * - * @return {Object} - * @api public */ get header() { return this.req.headers; - }, + } /** * Set request header. - * - * @api public */ set header(val) { this.req.headers = val; - }, + } /** * Return request header, alias as request.header - * - * @return {Object} - * @api public */ get headers() { return this.req.headers; - }, + } /** * Set request header, alias as request.header - * - * @api public */ set headers(val) { this.req.headers = val; - }, + } /** * Get request URL. - * - * @return {String} - * @api public */ get url() { - return this.req.url; - }, + return this.req.url!; + } /** * Set request URL. - * - * @api public */ set url(val) { this.req.url = val; - }, + } /** * Get origin of URL. - * - * @return {String} - * @api public */ get origin() { return `${this.protocol}://${this.host}`; - }, + } /** * Get full request URL. - * - * @return {String} - * @api public */ get href() { // support: `GET http://example.com/foo` if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl; return this.origin + this.originalUrl; - }, + } /** * Get request method. - * - * @return {String} - * @api public */ get method() { - return this.req.method; - }, + return this.req.method!; + } /** * Set request method. - * - * @param {String} val - * @api public */ set method(val) { this.req.method = val; - }, + } /** * Get request pathname. - * - * @return {String} - * @api public */ get path() { - return parse(this.req).pathname; - }, + return parse(this.req).pathname as string; + } /** * Set pathname, retaining the query string when present. - * - * @param {String} path - * @api public */ set path(path) { @@ -160,49 +133,44 @@ module.exports = { url.path = null; this.url = stringify(url); - }, + } + + #parsedUrlQueryCache: Record; /** * Get parsed query string. - * - * @return {Object} - * @api public */ - get query() { const str = this.querystring; - const c = this._querycache = this._querycache || {}; - return c[str] || (c[str] = qs.parse(str)); - }, + if (!this.#parsedUrlQueryCache) { + this.#parsedUrlQueryCache = {}; + } + let parsedUrlQuery = this.#parsedUrlQueryCache[str]; + if (!parsedUrlQuery) { + parsedUrlQuery = this.#parsedUrlQueryCache[str] = qs.parse(str); + } + return parsedUrlQuery; + } /** * Set query string as an object. - * - * @param {Object} obj - * @api public */ set query(obj) { this.querystring = qs.stringify(obj); - }, + } /** * Get query string. - * - * @return {String} - * @api public */ get querystring() { if (!this.req) return ''; return parse(this.req).query || ''; - }, + } /** * Set query string. - * - * @param {String} str - * @api public */ set querystring(str) { @@ -211,180 +179,139 @@ module.exports = { url.search = str; url.path = null; - this.url = stringify(url); - }, + } /** * Get the search string. Same as the query string * except it includes the leading ?. - * - * @return {String} - * @api public */ get search() { if (!this.querystring) return ''; return `?${this.querystring}`; - }, + } /** * Set the search string. Same as * request.querystring= but included for ubiquity. - * - * @param {String} str - * @api public */ set search(str) { this.querystring = str; - }, + } /** * Parse the "Host" header field host * and support X-Forwarded-Host when a * proxy is enabled. - * - * @return {String} hostname:port - * @api public + * return `hostname:port` format */ - get host() { const proxy = this.app.proxy; - let host = proxy && this.get('X-Forwarded-Host'); + let host = proxy ? this.get('X-Forwarded-Host') : ''; if (!host) { if (this.req.httpVersionMajor >= 2) host = this.get(':authority'); if (!host) host = this.get('Host'); } if (!host) return ''; return host.split(/\s*,\s*/, 1)[0]; - }, + } /** * Parse the "Host" header field hostname * and support X-Forwarded-Host when a * proxy is enabled. - * - * @return {String} hostname - * @api public */ - get hostname() { const host = this.host; if (!host) return ''; - if ('[' === host[0]) return this.URL.hostname || ''; // IPv6 + if (host[0] === '[') return this.URL.hostname || ''; // IPv6 return host.split(':', 1)[0]; - }, + } + + #memoizedURL: URL; /** * Get WHATWG parsed URL. * Lazily memoized. - * - * @return {URL|Object} - * @api public */ - get URL() { - /* istanbul ignore else */ - if (!this.memoizedURL) { + if (!this.#memoizedURL) { const originalUrl = this.originalUrl || ''; // avoid undefined in template string try { - this.memoizedURL = new URL(`${this.origin}${originalUrl}`); - } catch (err) { - this.memoizedURL = Object.create(null); + this.#memoizedURL = new URL(`${this.origin}${originalUrl}`); + } catch { + this.#memoizedURL = Object.create(null); } } - return this.memoizedURL; - }, + return this.#memoizedURL; + } /** * Check if the request is fresh, aka * Last-Modified and/or the ETag * still match. - * - * @return {Boolean} - * @api public */ - get fresh() { const method = this.method; - const s = this.ctx.status; + const status = this.response.status; // GET or HEAD for weak freshness validation only - if ('GET' !== method && 'HEAD' !== method) return false; + if (method !== 'GET' && method !== 'HEAD') return false; // 2xx or 304 as per rfc2616 14.26 - if ((s >= 200 && s < 300) || 304 === s) { + if ((status >= 200 && status < 300) || status === 304) { return fresh(this.header, this.response.header); } return false; - }, + } /** * Check if the request is stale, aka * "Last-Modified" and / or the "ETag" for the * resource has changed. - * - * @return {Boolean} - * @api public */ - get stale() { return !this.fresh; - }, + } /** * Check if the request is idempotent. - * - * @return {Boolean} - * @api public */ - get idempotent() { - const methods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']; - return !!~methods.indexOf(this.method); - }, + const methods = [ 'GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE' ]; + return methods.includes(this.method); + } /** * Return the request socket. - * - * @return {Connection} - * @api public */ - get socket() { - return this.req.socket; - }, + return this.req.socket as (Socket & { encrypted: boolean; }); + } /** * Get the charset when present or undefined. - * - * @return {String} - * @api public */ - get charset() { try { const { parameters } = contentType.parse(this.req); return parameters.charset || ''; - } catch (e) { + } catch { return ''; } - }, + } /** * Return parsed Content-Length when present. - * - * @return {Number} - * @api public */ - get length() { - const len = this.get('Content-Length'); + const len = this.get('Content-Length'); if (len === '') return; - return ~~len; - }, + return parseInt(len); + } /** * Return the protocol string "http" or "https" @@ -393,30 +320,22 @@ module.exports = { * field will be trusted. If you're running behind * a reverse proxy that supplies https for you this * may be enabled. - * - * @return {String} - * @api public */ - get protocol() { if (this.socket.encrypted) return 'https'; if (!this.app.proxy) return 'http'; - const proto = this.get('X-Forwarded-Proto'); + const proto = this.get('X-Forwarded-Proto'); return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http'; - }, + } /** * Shorthand for: * * this.protocol == 'https' - * - * @return {Boolean} - * @api public */ - get secure() { - return 'https' === this.protocol; - }, + return this.protocol === 'https'; + } /** * When `app.proxy` is `true`, parse @@ -425,14 +344,10 @@ module.exports = { * For example if the value was "client, proxy1, proxy2" * you would receive the array `["client", "proxy1", "proxy2"]` * where "proxy2" is the furthest down-stream. - * - * @return {Array} - * @api public */ - get ips() { const proxy = this.app.proxy; - const val = this.get(this.app.proxyIpHeader); + const val = this.get(this.app.proxyIpHeader); let ips = proxy && val ? val.split(/\s*,\s*/) : []; @@ -440,27 +355,24 @@ module.exports = { ips = ips.slice(-this.app.maxIpsCount); } return ips; - }, + } + #ip: string; /** * Return request's remote address * When `app.proxy` is `true`, parse * the "X-Forwarded-For" ip address list and return the first one - * - * @return {String} - * @api public */ - get ip() { - if (!this[IP]) { - this[IP] = this.ips[0] || this.socket.remoteAddress || ''; + if (!this.#ip) { + this.#ip = this.ips[0] || this.socket.remoteAddress || ''; } - return this[IP]; - }, + return this.#ip; + } - set ip(_ip) { - this[IP] = _ip; - }, + set ip(ip: string) { + this.#ip = ip; + } /** * Return subdomains as an array. @@ -473,11 +385,7 @@ module.exports = { * If `app.subdomainOffset` is not set, this.subdomains is * `["ferrets", "tobi"]`. * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`. - * - * @return {Array} - * @api public */ - get subdomains() { const offset = this.app.subdomainOffset; const hostname = this.hostname; @@ -486,30 +394,23 @@ module.exports = { .split('.') .reverse() .slice(offset); - }, + } + #accept: any; /** * Get accept object. * Lazily memoized. - * - * @return {Object} - * @api private */ - get accept() { - return this._accept || (this._accept = accepts(this.req)); - }, + return this.#accept || (this.#accept = accepts(this.req)); + } /** * Set accept object. - * - * @param {Object} - * @api private */ - set accept(obj) { - this._accept = obj; - }, + this.#accept = obj; + } /** * Check if the given `type(s)` is acceptable, returning @@ -546,15 +447,10 @@ module.exports = { * this.accepts(['html', 'json']); * this.accepts('html', 'json'); * // => "json" - * - * @param {String|Array} type(s)... - * @return {String|Array|false} - * @api public */ - - accepts(...args) { + accepts(...args: any[]): string | string[] | false { return this.accept.types(...args); - }, + } /** * Return accepted encodings or best fit based on `encodings`. @@ -563,15 +459,10 @@ module.exports = { * an array sorted by quality is returned: * * ['gzip', 'deflate'] - * - * @param {String|Array} encoding(s)... - * @return {String|Array} - * @api public */ - - acceptsEncodings(...args) { + acceptsEncodings(...args: any[]): string | string[] { return this.accept.encodings(...args); - }, + } /** * Return accepted charsets or best fit based on `charsets`. @@ -580,15 +471,10 @@ module.exports = { * an array sorted by quality is returned: * * ['utf-8', 'utf-7', 'iso-8859-1'] - * - * @param {String|Array} charset(s)... - * @return {String|Array} - * @api public */ - - acceptsCharsets(...args) { + acceptsCharsets(...args: any[]): string | string[] { return this.accept.charsets(...args); - }, + } /** * Return accepted languages or best fit based on `langs`. @@ -597,15 +483,10 @@ module.exports = { * an array sorted by quality is returned: * * ['es', 'pt', 'en'] - * - * @param {String|Array} lang(s)... - * @return {Array|String} - * @api public */ - - acceptsLanguages(...args) { + acceptsLanguages(...args: any[]): string | string[] { return this.accept.languages(...args); - }, + } /** * Check if the incoming request contains the "Content-Type" @@ -627,30 +508,20 @@ module.exports = { * this.is('html', 'application/*'); // => 'application/json' * * this.is('html'); // => false - * - * @param {String|String[]} [type] - * @param {String[]} [types] - * @return {String|false|null} - * @api public */ - - is(type, ...types) { + is(type?: string | string[], ...types: string[]): string | false | null { return typeis(this.req, type, ...types); - }, + } /** * Return the request mime type void of * parameters such as "charset". - * - * @return {String} - * @api public */ - get type() { - const type = this.get('Content-Type'); + const type = this.get('Content-Type'); if (!type) return ''; return type.split(';')[0]; - }, + } /** * Return request header. @@ -668,59 +539,41 @@ module.exports = { * * this.get('Something'); * // => '' - * - * @param {String} field - * @return {String} - * @api public */ - - get(field) { + get(field: string): T { const req = this.req; switch (field = field.toLowerCase()) { case 'referer': case 'referrer': - return req.headers.referrer || req.headers.referer || ''; + return (req.headers.referrer || req.headers.referer || '') as T; default: - return req.headers[field] || ''; + return (req.headers[field] || '') as T; } - }, + } /** * Inspect implementation. - * - * @return {Object} - * @api public */ - inspect() { if (!this.req) return; return this.toJSON(); - }, + } /** - * Return JSON representation. - * - * @return {Object} - * @api public + * Custom inspection implementation for newer Node.js versions. */ + [util.inspect.custom]() { + return this.inspect(); + } + /** + * Return JSON representation. + */ toJSON() { return only(this, [ 'method', 'url', - 'header' + 'header', ]); } -}; - -/** - * Custom inspection implementation for newer Node.js versions. - * - * @return {Object} - * @api public - */ - -/* istanbul ignore else */ -if (util.inspect.custom) { - module.exports[util.inspect.custom] = module.exports.inspect; } diff --git a/lib/response.js b/src/response.ts similarity index 65% rename from lib/response.js rename to src/response.ts index 54f9d49f7..d9115ef58 100644 --- a/lib/response.js +++ b/src/response.ts @@ -1,142 +1,112 @@ - -'use strict'; - -/** - * Module dependencies. - */ - -const contentDisposition = require('content-disposition'); -const getType = require('cache-content-type'); -const onFinish = require('on-finished'); -const escape = require('escape-html'); -const typeis = require('type-is').is; -const statuses = require('statuses'); -const destroy = require('destroy'); -const assert = require('assert'); -const extname = require('path').extname; -const vary = require('vary'); -const only = require('only'); -const util = require('util'); -const encodeUrl = require('encodeurl'); -const Stream = require('stream'); - -/** - * Prototype. - */ - -module.exports = { +import assert from 'node:assert'; +import { extname } from 'node:path'; +import util from 'node:util'; +import Stream from 'node:stream'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import contentDisposition from 'content-disposition'; +import getType from 'cache-content-type'; +import onFinish from 'on-finished'; +import escape from 'escape-html'; +import { is as typeis } from 'type-is'; +import statuses from 'statuses'; +import destroy from 'destroy'; +import vary from 'vary'; +import only from 'only'; +import encodeUrl from 'encodeurl'; +import type Application from './application'; +import type { ContextDelegation } from './context'; +import type Request from './request'; + +export default class Response { + app: Application; + req: IncomingMessage; + res: ServerResponse; + ctx: ContextDelegation; + request: Request; + + constructor(app: Application, ctx: ContextDelegation, req: IncomingMessage, res: ServerResponse) { + this.app = app; + this.req = req; + this.res = res; + this.ctx = ctx; + } /** * Return the request socket. - * - * @return {Connection} - * @api public */ - get socket() { return this.res.socket; - }, + } /** * Return response header. - * - * @return {Object} - * @api public */ - get header() { - const { res } = this; - return typeof res.getHeaders === 'function' - ? res.getHeaders() - : res._headers || {}; // Node < 7.7 - }, + return this.res.getHeaders() || {}; + } /** * Return response header, alias as response.header - * - * @return {Object} - * @api public */ - get headers() { return this.header; - }, + } + + _explicitStatus: boolean; /** * Get response status code. - * - * @return {Number} - * @api public */ - get status() { return this.res.statusCode; - }, + } /** * Set response status code. - * - * @param {Number} code - * @api public */ - - set status(code) { + set status(code: number) { if (this.headerSent) return; - assert(Number.isInteger(code), 'status code must be a number'); assert(code >= 100 && code <= 999, `invalid status code: ${code}`); this._explicitStatus = true; this.res.statusCode = code; if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]; if (this.body && statuses.empty[code]) this.body = null; - }, + } /** * Get response status message - * - * @return {String} - * @api public */ - - get message() { + get message(): string { return this.res.statusMessage || statuses[this.status]; - }, + } /** * Set response status message - * - * @param {String} msg - * @api public */ - - set message(msg) { + set message(msg: string) { this.res.statusMessage = msg; - }, + } + + _body: any; + _explicitNullBody: boolean; /** * Get response body. - * - * @return {Mixed} - * @api public */ - get body() { return this._body; - }, + } /** * Set response body. - * - * @param {String|Buffer|Object|Stream} val - * @api public */ - - set body(val) { + set body(val: string | Buffer | object | Stream | null | undefined | boolean) { const original = this._body; this._body = val; // no content - if (null == val) { + if (val == null) { if (!statuses.empty[this.status]) this.status = 204; if (val === null) this._explicitNullBody = true; this.remove('Content-Type'); @@ -152,7 +122,7 @@ module.exports = { const setType = !this.has('Content-Type'); // string - if ('string' === typeof val) { + if (typeof val === 'string') { if (setType) this.type = /^\s* this.ctx.onerror(err)); // overwriting - if (null != original) this.remove('Content-Length'); + if (original != null) this.remove('Content-Length'); } if (setType) this.type = 'bin'; @@ -181,28 +152,21 @@ module.exports = { // json this.remove('Content-Length'); this.type = 'json'; - }, + } /** * Set Content-Length field to `n`. - * - * @param {Number} n - * @api public */ - - set length(n) { + set length(n: number | string | undefined) { + if (n === undefined) return; if (!this.has('Transfer-Encoding')) { this.set('Content-Length', n); } - }, + } /** * Return parsed response Content-Length when present. - * - * @return {Number} - * @api public */ - get length() { if (this.has('Content-Length')) { return parseInt(this.get('Content-Length'), 10) || 0; @@ -210,34 +174,25 @@ module.exports = { const { body } = this; if (!body || body instanceof Stream) return undefined; - if ('string' === typeof body) return Buffer.byteLength(body); + if (typeof body === 'string') return Buffer.byteLength(body); if (Buffer.isBuffer(body)) return body.length; return Buffer.byteLength(JSON.stringify(body)); - }, + } /** * Check if a header has been written to the socket. - * - * @return {Boolean} - * @api public */ - get headerSent() { return this.res.headersSent; - }, + } /** * Vary on `field`. - * - * @param {String} field - * @api public */ - - vary(field) { + vary(field: string) { if (this.headerSent) return; - vary(this.res, field); - }, + } /** * Perform a 302 redirect to `url`. @@ -252,15 +207,10 @@ module.exports = { * this.redirect('back', '/index.html'); * this.redirect('/login'); * this.redirect('http://google.com'); - * - * @param {String} url - * @param {String} [alt] - * @api public */ - - redirect(url, alt) { + redirect(url: string, alt?: string) { // location - if ('back' === url) url = this.ctx.get('Referrer') || alt || '/'; + if (url === 'back') url = this.ctx.get('Referrer') || alt || '/'; this.set('Location', encodeUrl(url)); // status @@ -277,19 +227,15 @@ module.exports = { // text this.type = 'text/plain; charset=utf-8'; this.body = `Redirecting to ${url}.`; - }, + } /** * Set Content-Disposition header to "attachment" with optional `filename`. - * - * @param {String} filename - * @api public */ - - attachment(filename, options) { + attachment(filename?: string, options?: any) { if (filename) this.type = extname(filename); this.set('Content-Disposition', contentDisposition(filename, options)); - }, + } /** * Set Content-Type response header with `type` through `mime.lookup()` @@ -302,46 +248,54 @@ module.exports = { * this.type = 'json'; * this.type = 'application/json'; * this.type = 'png'; - * - * @param {String} type - * @api public */ - - set type(type) { + set type(type: string | null | undefined) { type = getType(type); if (type) { this.set('Content-Type', type); } else { this.remove('Content-Type'); } - }, + } + + /** + * Return the response mime type void of + * parameters such as "charset". + */ + get type() { + const type = this.get('Content-Type'); + if (!type) return ''; + return type.split(';', 1)[0]; + } + + /** + * Check whether the response is one of the listed types. + * Pretty much the same as `this.request.is()`. + */ + is(type?: string | string[], ...types: string[]): string | false { + return typeis(this.type, type, ...types); + } /** * Set the Last-Modified date using a string or a Date. * * this.response.lastModified = new Date(); * this.response.lastModified = '2013-09-13'; - * - * @param {String|Date} type - * @api public */ - - set lastModified(val) { - if ('string' === typeof val) val = new Date(val); - this.set('Last-Modified', val.toUTCString()); - }, + set lastModified(val: string | Date | undefined) { + if (typeof val === 'string') val = new Date(val); + if (val) { + this.set('Last-Modified', val.toUTCString()); + } + } /** * Get the Last-Modified date in Date form, if it exists. - * - * @return {Date} - * @api public */ - - get lastModified() { - const date = this.get('last-modified'); + get lastModified(): Date | undefined { + const date = this.get('last-modified'); if (date) return new Date(date); - }, + } /** * Set the ETag of a response. @@ -350,54 +304,18 @@ module.exports = { * this.response.etag = 'md5hashsum'; * this.response.etag = '"md5hashsum"'; * this.response.etag = 'W/"123456789"'; - * - * @param {String} etag - * @api public */ - - set etag(val) { + set etag(val: string) { if (!/^(W\/)?"/.test(val)) val = `"${val}"`; this.set('ETag', val); - }, + } /** * Get the ETag of a response. - * - * @return {String} - * @api public */ - get etag() { return this.get('ETag'); - }, - - /** - * Return the response mime type void of - * parameters such as "charset". - * - * @return {String} - * @api public - */ - - get type() { - const type = this.get('Content-Type'); - if (!type) return ''; - return type.split(';', 1)[0]; - }, - - /** - * Check whether the response is one of the listed types. - * Pretty much the same as `this.request.is()`. - * - * @param {String|String[]} [type] - * @param {String[]} [types] - * @return {String|false} - * @api public - */ - - is(type, ...types) { - return typeis(this.type, type, ...types); - }, + } /** * Return response header. @@ -409,15 +327,10 @@ module.exports = { * * this.get('content-type'); * // => "text/plain" - * - * @param {String} field - * @return {String} - * @api public */ - - get(field) { - return this.header[field.toLowerCase()] || ''; - }, + get(field: string): T { + return (this.header[field.toLowerCase()] || '') as T; + } /** * Returns true if the header identified by name is currently set in the outgoing headers. @@ -430,18 +343,10 @@ module.exports = { * * this.get('content-type'); * // => true - * - * @param {String} field - * @return {boolean} - * @api public */ - - has(field) { - return typeof this.res.hasHeader === 'function' - ? this.res.hasHeader(field) - // Node < 7.7 - : field.toLowerCase() in this.headers; - }, + has(field: string) { + return this.res.hasHeader(field); + } /** * Set header `field` to `val` or pass @@ -452,25 +357,24 @@ module.exports = { * this.set('Foo', ['bar', 'baz']); * this.set('Accept', 'application/json'); * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * @param {String|Object|Array} field - * @param {String} val - * @api public */ - - set(field, val) { + set(field: string | object, val?: string | number | any[]) { if (this.headerSent) return; - - if (2 === arguments.length) { - if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v)); - else if (typeof val !== 'string') val = String(val); + if (typeof field === 'string') { + if (Array.isArray(val)) { + val = val.map(v => { + return typeof v === 'string' ? v : String(v); + }); + } else if (typeof val !== 'string') { + val = String(val); + } this.res.setHeader(field, val); } else { for (const key in field) { this.set(key, field[key]); } } - }, + } /** * Append additional header `field` with value `val`. @@ -481,47 +385,33 @@ module.exports = { * this.append('Link', ['', '']); * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); * this.append('Warning', '199 Miscellaneous warning'); - * ``` - * - * @param {String} field - * @param {String|Array} val - * @api public */ - - append(field, val) { + append(field: string, val: string | string[]) { const prev = this.get(field); + let value: any | any[] = val; if (prev) { - val = Array.isArray(prev) - ? prev.concat(val) - : [prev].concat(val); + value = Array.isArray(prev) + ? prev.concat(value) + : [ prev ].concat(val); } - return this.set(field, val); - }, + return this.set(field, value); + } /** * Remove header `field`. - * - * @param {String} name - * @api public */ - - remove(field) { + remove(field: string) { if (this.headerSent) return; - this.res.removeHeader(field); - }, + } /** * Checks if the request is writable. * Tests for the existence of the socket * as node sometimes does not set it. - * - * @return {Boolean} - * @api private */ - get writable() { // can't write any more after response finished // response.writableEnded is available since Node > 12.9 @@ -535,54 +425,37 @@ module.exports = { // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 if (!socket) return true; return socket.writable; - }, + } /** * Inspect implementation. - * - * @return {Object} - * @api public */ - inspect() { if (!this.res) return; const o = this.toJSON(); o.body = this.body; return o; - }, + } + + [util.inspect.custom]() { + return this.inspect(); + } /** * Return JSON representation. - * - * @return {Object} - * @api public */ - toJSON() { return only(this, [ 'status', 'message', - 'header' + 'header', ]); - }, + } /** * Flush any set headers and begin the body */ - flushHeaders() { this.res.flushHeaders(); } -}; - -/** - * Custom inspection implementation for node 6+. - * - * @return {Object} - * @api public - */ - -/* istanbul ignore else */ -if (util.inspect.custom) { - module.exports[util.inspect.custom] = module.exports.inspect; } diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 000000000..5646d8c69 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,13 @@ +export type CustomError = Error & { + headers?: object; + status?: number; + statusCode?: number; + code?: string; + expose?: boolean; +}; + +export type ProtoImplClass = new(...args: any[]) => T; + +export type AnyProto = { + [key: string]: any; +}; diff --git a/test-helpers/context.js b/test-helpers/context.js deleted file mode 100644 index 0dd0238c5..000000000 --- a/test-helpers/context.js +++ /dev/null @@ -1,21 +0,0 @@ - -'use strict'; - -const Stream = require('stream'); -const Koa = require('../lib/application'); - -module.exports = (req, res, app) => { - const socket = new Stream.Duplex(); - req = Object.assign({ headers: {}, socket }, Stream.Readable.prototype, req); - res = Object.assign({ _headers: {}, socket }, Stream.Writable.prototype, res); - req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1'; - app = app || new Koa(); - res.getHeader = k => res._headers[k.toLowerCase()]; - res.setHeader = (k, v) => res._headers[k.toLowerCase()] = v; - res.removeHeader = (k, v) => delete res._headers[k.toLowerCase()]; - return app.createContext(req, res); -}; - -module.exports.request = (req, res, app) => module.exports(req, res, app).request; - -module.exports.response = (req, res, app) => module.exports(req, res, app).response; diff --git a/test/application/context.test.ts b/test/application/context.test.ts new file mode 100644 index 000000000..d9fca3623 --- /dev/null +++ b/test/application/context.test.ts @@ -0,0 +1,42 @@ +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; + +describe('app.context', () => { + const app1 = new Koa(); + app1.context.msg = 'hello app1'; + const app2 = new Koa(); + app2.request.foo = 'bar'; + + it('should the context between apps is isolated', () => { + assert.notStrictEqual(app1.context, app2.context); + assert.strictEqual(app1.context.msg, 'hello app1'); + assert.strictEqual(app1.request.foo, undefined); + assert.strictEqual(app2.context.msg, undefined); + assert.strictEqual(app2.request.foo, 'bar'); + }); + + it('should merge properties', () => { + app1.use(ctx => { + assert.strictEqual(ctx.msg, 'hello app1'); + assert.strictEqual((ctx.request as any).foo, undefined); + ctx.status = 204; + }); + + return request(app1.listen()) + .get('/') + .expect(204); + }); + + it('should not affect the original prototype', () => { + app2.use(ctx => { + assert.strictEqual(ctx.msg, undefined); + assert.strictEqual((ctx.request as any).foo, 'bar'); + ctx.status = 204; + }); + + return request(app2.listen()) + .get('/') + .expect(204); + }); +}); diff --git a/test/application/currentContext.test.ts b/test/application/currentContext.test.ts new file mode 100644 index 000000000..6a1701c41 --- /dev/null +++ b/test/application/currentContext.test.ts @@ -0,0 +1,66 @@ + +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; + +describe('app.currentContext', () => { + it('should get currentContext', async () => { + const app = new Koa({}); + + app.use(async ctx => { + assert(ctx === app.currentContext); + await new Promise(resolve => { + setTimeout(() => { + assert(ctx === app.currentContext); + resolve(); + }, 1); + }); + await new Promise(resolve => { + assert(ctx === app.currentContext); + setImmediate(() => { + assert(ctx === app.currentContext); + resolve(); + }); + }); + assert(ctx === app.currentContext); + app.currentContext.body = 'ok'; + }); + + const requestServer = async () => { + assert(app.currentContext === undefined); + await request(app.callback()).get('/').expect('ok'); + assert(app.currentContext === undefined); + }; + + await Promise.all([ + requestServer(), + requestServer(), + requestServer(), + requestServer(), + requestServer(), + ]); + }); + + it('should get currentContext work', async () => { + const app = new Koa({}); + + app.use(async () => { + throw new Error('error message'); + }); + + const handleError = new Promise((resolve, reject) => { + app.on('error', (err, ctx) => { + try { + assert.strictEqual(err.message, 'error message'); + assert.strictEqual(app.currentContext, ctx); + resolve(); + } catch (e) { + reject(e); + } + }); + }); + + await request(app.callback()).get('/').expect('Internal Server Error'); + await handleError; + }); +}); diff --git a/__tests__/application/index.js b/test/application/index.test.ts similarity index 83% rename from __tests__/application/index.js rename to test/application/index.test.ts index 2e8846993..d38c2ee38 100644 --- a/__tests__/application/index.js +++ b/test/application/index.test.ts @@ -1,16 +1,14 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import CreateError from 'http-errors'; +import Koa from '../..'; describe('app', () => { // ignore test on Node.js v18 - (/^v18\./.test(process.version) ? it.skip : it)('should handle socket errors', done => { + it('should handle socket errors', done => { const app = new Koa(); - app.use((ctx, next) => { + app.use(ctx => { // triggers ctx.socket.writable == false ctx.socket.emit('error', new Error('boom')); }); @@ -22,15 +20,17 @@ describe('app', () => { request(app.callback()) .get('/') - .end(() => {}); + .end(() => { + // empty + }); }); it('should not .writeHead when !socket.writable', done => { const app = new Koa(); - app.use((ctx, next) => { + app.use(ctx => { // set .writable to false - ctx.socket.writable = false; + (ctx.socket as any).writable = false; ctx.status = 204; // throw if .writeHead or .end is called ctx.res.writeHead = @@ -44,7 +44,9 @@ describe('app', () => { request(app.callback()) .get('/') - .end(() => {}); + .end(() => { + // empty + }); }); it('should set development env when NODE_ENV missing', () => { @@ -68,7 +70,7 @@ describe('app', () => { }); it('should set signed cookie keys from the constructor', () => { - const keys = ['customkey']; + const keys = [ 'customkey' ]; const app = new Koa({ keys }); assert.strictEqual(app.keys, keys); }); @@ -80,8 +82,6 @@ describe('app', () => { }); it('should have a static property exporting `HttpError` from http-errors library', () => { - const CreateError = require('http-errors'); - assert.notEqual(Koa.HttpError, undefined); assert.deepStrictEqual(Koa.HttpError, CreateError.HttpError); assert.throws(() => { throw new CreateError(500, 'test error'); }, Koa.HttpError); diff --git a/__tests__/application/inspect.js b/test/application/inspect.test.ts similarity index 73% rename from __tests__/application/inspect.js rename to test/application/inspect.test.ts index 85adfba6a..8f7fd122d 100644 --- a/__tests__/application/inspect.js +++ b/test/application/inspect.test.ts @@ -1,9 +1,7 @@ -'use strict'; - -const assert = require('assert'); -const util = require('util'); -const Koa = require('../..'); +import assert from 'node:assert'; +import util from 'node:util'; +import Koa from '../..'; const app = new Koa(); describe('app.inspect()', () => { @@ -15,7 +13,7 @@ describe('app.inspect()', () => { it('should return a json representation', () => { assert.deepStrictEqual( { subdomainOffset: 2, proxy: false, env: 'test' }, - app.inspect() + app.inspect(), ); }); }); diff --git a/__tests__/application/onerror.js b/test/application/onerror.test.ts similarity index 50% rename from __tests__/application/onerror.js rename to test/application/onerror.test.ts index 40d1299bc..c9910d792 100644 --- a/__tests__/application/onerror.js +++ b/test/application/onerror.test.ts @@ -1,51 +1,49 @@ - -'use strict'; - -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import { runInNewContext } from 'node:vm'; +import mm from 'mm'; +import Koa from '../..'; describe('app.onerror(err)', () => { + afterEach(mm.restore); + it('should throw an error if a non-error is given', () => { const app = new Koa(); assert.throws(() => { - app.onerror('foo'); + (app as any).onerror('foo' as any); }, TypeError, 'non-error thrown: foo'); }); it('should accept errors coming from other scopes', () => { - const ExternError = require('vm').runInNewContext('Error'); - const app = new Koa(); + const ExternError = runInNewContext('Error'); const error = Object.assign(new ExternError('boom'), { status: 418, - expose: true + expose: true, }); - assert.doesNotThrow(() => app.onerror(error)); + assert.doesNotThrow(() => (app as any).onerror(error)); }); it('should do nothing if status is 404', () => { const app = new Koa(); const err = new Error(); - err.status = 404; + (err as any).status = 404; - const spy = jest.spyOn(console, 'error'); - app.onerror(err); - expect(spy).not.toHaveBeenCalled(); - spy.mockRestore(); + mm.spy(console, 'error'); + (app as any).onerror(err); + assert.strictEqual((console.error as any).called, undefined); }); it('should do nothing if .silent', () => { const app = new Koa(); - app.silent = true; + (app as any).silent = true; const err = new Error(); - const spy = jest.spyOn(console, 'error'); - app.onerror(err); - expect(spy).not.toHaveBeenCalled(); - spy.mockRestore(); + mm.spy(console, 'error'); + (app as any).onerror(err); + assert.strictEqual((console.error as any).called, undefined); }); it('should log the error to stderr', () => { @@ -55,9 +53,8 @@ describe('app.onerror(err)', () => { const err = new Error(); err.stack = 'Foo'; - const spy = jest.spyOn(console, 'error'); - app.onerror(err); - expect(spy).toHaveBeenCalled(); - spy.mockRestore(); + mm.spy(console, 'error'); + (app as any).onerror(err); + assert.strictEqual((console.error as any).called, 1); }); }); diff --git a/__tests__/application/request.js b/test/application/request.test.ts similarity index 75% rename from __tests__/application/request.js rename to test/application/request.test.ts index 3e79887c5..f998bb823 100644 --- a/__tests__/application/request.js +++ b/test/application/request.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; describe('app.request', () => { const app1 = new Koa(); @@ -11,7 +8,7 @@ describe('app.request', () => { const app2 = new Koa(); it('should merge properties', () => { - app1.use((ctx, next) => { + app1.use(ctx => { assert.strictEqual(ctx.request.message, 'hello'); ctx.status = 204; }); @@ -22,7 +19,7 @@ describe('app.request', () => { }); it('should not affect the original prototype', () => { - app2.use((ctx, next) => { + app2.use(ctx => { assert.strictEqual(ctx.request.message, undefined); ctx.status = 204; }); diff --git a/__tests__/application/respond.js b/test/application/respond.test.ts similarity index 91% rename from __tests__/application/respond.js rename to test/application/respond.test.ts index 26e6052f4..016cb6e59 100644 --- a/__tests__/application/respond.js +++ b/test/application/respond.test.ts @@ -1,11 +1,9 @@ - -'use strict'; - -const request = require('supertest'); -const statuses = require('statuses'); -const assert = require('assert'); -const Koa = require('../..'); -const fs = require('fs'); +/* eslint-disable @typescript-eslint/no-var-requires */ +import assert from 'node:assert'; +import fs from 'node:fs'; +import request from 'supertest'; +import statuses from 'statuses'; +import Koa from '../..'; describe('app.respond', () => { describe('when ctx.respond === false', () => { @@ -82,7 +80,7 @@ describe('app.respond', () => { }); describe('when this.type === null', () => { - it('should not send Content-Type header', async() => { + it('should not send Content-Type header', async () => { const app = new Koa(); app.use(ctx => { @@ -101,7 +99,7 @@ describe('app.respond', () => { }); describe('when HEAD is used', () => { - it('should not respond with the body', async() => { + it('should not respond with the body', async () => { const app = new Koa(); app.use(ctx => { @@ -119,7 +117,7 @@ describe('app.respond', () => { assert(!res.text); }); - it('should keep json headers', async() => { + it('should keep json headers', async () => { const app = new Koa(); app.use(ctx => { @@ -137,7 +135,7 @@ describe('app.respond', () => { assert(!res.text); }); - it('should keep string headers', async() => { + it('should keep string headers', async () => { const app = new Koa(); app.use(ctx => { @@ -155,7 +153,7 @@ describe('app.respond', () => { assert(!res.text); }); - it('should keep buffer headers', async() => { + it('should keep buffer headers', async () => { const app = new Koa(); app.use(ctx => { @@ -173,7 +171,7 @@ describe('app.respond', () => { assert(!res.text); }); - it('should keep stream header if set manually', async() => { + it('should keep stream header if set manually', async () => { const app = new Koa(); const { length } = fs.readFileSync('package.json'); @@ -189,15 +187,15 @@ describe('app.respond', () => { .head('/') .expect(200); - assert.strictEqual(~~res.header['content-length'], length); + assert.strictEqual(parseInt(res.header['content-length']), length); assert(!res.text); }); it('should respond with a 404 if no body was set', () => { const app = new Koa(); - app.use(ctx => { - + app.use(() => { + // empty }); const server = app.listen(); @@ -254,7 +252,7 @@ describe('app.respond', () => { it('should not cause an app error', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use(ctx => { const res = ctx.res; ctx.status = 200; res.setHeader('Content-Type', 'text/html'); @@ -273,12 +271,12 @@ describe('app.respond', () => { it('should send the right body', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use(ctx => { const res = ctx.res; ctx.status = 200; res.setHeader('Content-Type', 'text/html'); res.write('Hello'); - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { res.end('Goodbye'); resolve(); @@ -315,7 +313,7 @@ describe('app.respond', () => { }); describe('with status=204', () => { - it('should respond without a body', async() => { + it('should respond without a body', async () => { const app = new Koa(); app.use(ctx => { @@ -334,7 +332,7 @@ describe('app.respond', () => { }); describe('with status=205', () => { - it('should respond without a body', async() => { + it('should respond without a body', async () => { const app = new Koa(); app.use(ctx => { @@ -353,7 +351,7 @@ describe('app.respond', () => { }); describe('with status=304', () => { - it('should respond without a body', async() => { + it('should respond without a body', async () => { const app = new Koa(); app.use(ctx => { @@ -372,7 +370,7 @@ describe('app.respond', () => { }); describe('with custom status=700', () => { - it('should respond with the associated status message', async() => { + it('should respond with the associated status message', async () => { const app = new Koa(); statuses['700'] = 'custom status'; @@ -392,7 +390,7 @@ describe('app.respond', () => { }); describe('with custom statusMessage=ok', () => { - it('should respond with the custom status message', async() => { + it('should respond with the custom status message', async () => { const app = new Koa(); app.use(ctx => { @@ -430,7 +428,7 @@ describe('app.respond', () => { }); describe('when .body is a null', () => { - it('should respond 204 by default', async() => { + it('should respond 204 by default', async () => { const app = new Koa(); app.use(ctx => { @@ -447,7 +445,7 @@ describe('app.respond', () => { assert.strictEqual(res.headers.hasOwnProperty('content-type'), false); }); - it('should respond 204 with status=200', async() => { + it('should respond 204 with status=200', async () => { const app = new Koa(); app.use(ctx => { @@ -465,7 +463,7 @@ describe('app.respond', () => { assert.strictEqual(res.headers.hasOwnProperty('content-type'), false); }); - it('should respond 205 with status=205', async() => { + it('should respond 205 with status=205', async () => { const app = new Koa(); app.use(ctx => { @@ -483,7 +481,7 @@ describe('app.respond', () => { assert.strictEqual(res.headers.hasOwnProperty('content-type'), false); }); - it('should respond 304 with status=304', async() => { + it('should respond 304 with status=304', async () => { const app = new Koa(); app.use(ctx => { @@ -536,7 +534,7 @@ describe('app.respond', () => { }); describe('when .body is a Stream', () => { - it('should respond', async() => { + it('should respond', async () => { const app = new Koa(); app.use(ctx => { @@ -555,7 +553,7 @@ describe('app.respond', () => { assert.deepStrictEqual(res.body, pkg); }); - it('should strip content-length when overwriting', async() => { + it('should strip content-length when overwriting', async () => { const app = new Koa(); app.use(ctx => { @@ -575,7 +573,7 @@ describe('app.respond', () => { assert.deepStrictEqual(res.body, pkg); }); - it('should keep content-length if not overwritten', async() => { + it('should keep content-length if not overwritten', async () => { const app = new Koa(); app.use(ctx => { @@ -596,7 +594,7 @@ describe('app.respond', () => { }); it('should keep content-length if overwritten with the same stream', - async() => { + async () => { const app = new Koa(); app.use(ctx => { @@ -711,7 +709,7 @@ describe('app.respond', () => { it('should emit "error" on the app', done => { const app = new Koa(); - app.use(ctx => { + app.use(() => { throw new Error('boom'); }); @@ -722,17 +720,19 @@ describe('app.respond', () => { request(app.callback()) .get('/') - .end(() => {}); + .end(() => { + // ignore + }); }); describe('with an .expose property', () => { it('should expose the message', () => { const app = new Koa(); - app.use(ctx => { + app.use(() => { const err = new Error('sorry!'); - err.status = 403; - err.expose = true; + (err as any).status = 403; + (err as any).expose = true; throw err; }); @@ -746,9 +746,9 @@ describe('app.respond', () => { it('should respond with .status', () => { const app = new Koa(); - app.use(ctx => { + app.use(() => { const err = new Error('s3 explodes'); - err.status = 403; + (err as any).status = 403; throw err; }); @@ -761,7 +761,7 @@ describe('app.respond', () => { it('should respond with 500', () => { const app = new Koa(); - app.use(ctx => { + app.use(() => { throw new Error('boom!'); }); @@ -783,7 +783,7 @@ describe('app.respond', () => { }); }); - app.use((ctx, next) => { + app.use(() => { throw new Error('boom!'); }); @@ -813,7 +813,7 @@ describe('app.respond', () => { .expect('hello'); }); - it('should 204', async() => { + it('should 204', async () => { const app = new Koa(); app.use(ctx => { @@ -834,7 +834,7 @@ describe('app.respond', () => { }); describe('with explicit null body', () => { - it('should preserve given status', async() => { + it('should preserve given status', async () => { const app = new Koa(); app.use(ctx => { @@ -850,7 +850,7 @@ describe('app.respond', () => { .expect('') .expect({}); }); - it('should respond with correct headers', async() => { + it('should respond with correct headers', async () => { const app = new Koa(); app.use(ctx => { diff --git a/__tests__/application/response.js b/test/application/response.test.ts similarity index 77% rename from __tests__/application/response.js rename to test/application/response.test.ts index 1ed5571e1..e32761ffe 100644 --- a/__tests__/application/response.js +++ b/test/application/response.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; describe('app.response', () => { const app1 = new Koa(); @@ -14,7 +11,7 @@ describe('app.response', () => { const app5 = new Koa(); it('should merge properties', () => { - app1.use((ctx, next) => { + app1.use(ctx => { assert.strictEqual(ctx.response.msg, 'hello'); ctx.status = 204; }); @@ -25,7 +22,7 @@ describe('app.response', () => { }); it('should not affect the original prototype', () => { - app2.use((ctx, next) => { + app2.use(ctx => { assert.strictEqual(ctx.response.msg, undefined); ctx.status = 204; }); @@ -35,8 +32,8 @@ describe('app.response', () => { .expect(204); }); - it('should not include status message in body for http2', async() => { - app3.use((ctx, next) => { + it('should not include status message in body for http2', async () => { + app3.use(ctx => { ctx.req.httpVersionMajor = 2; ctx.status = 404; }); @@ -46,8 +43,8 @@ describe('app.response', () => { assert.strictEqual(response.text, '404'); }); - it('should set ._explicitNullBody correctly', async() => { - app4.use((ctx, next) => { + it('should set ._explicitNullBody correctly', async () => { + app4.use(ctx => { ctx.body = null; assert.strictEqual(ctx.response._explicitNullBody, true); }); @@ -57,8 +54,8 @@ describe('app.response', () => { .expect(204); }); - it('should not set ._explicitNullBody incorrectly', async() => { - app5.use((ctx, next) => { + it('should not set ._explicitNullBody incorrectly', async () => { + app5.use(ctx => { ctx.body = undefined; assert.strictEqual(ctx.response._explicitNullBody, undefined); ctx.body = ''; diff --git a/__tests__/application/toJSON.js b/test/application/toJSON.test.ts similarity index 68% rename from __tests__/application/toJSON.js rename to test/application/toJSON.test.ts index 172fbaa79..30bae0ce5 100644 --- a/__tests__/application/toJSON.js +++ b/test/application/toJSON.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import Koa from '../..'; describe('app.toJSON()', () => { it('should work', () => { @@ -12,7 +9,7 @@ describe('app.toJSON()', () => { assert.deepStrictEqual({ subdomainOffset: 2, proxy: false, - env: 'test' + env: 'test', }, obj); }); }); diff --git a/test/application/use.test.ts b/test/application/use.test.ts new file mode 100644 index 000000000..c7427e1ec --- /dev/null +++ b/test/application/use.test.ts @@ -0,0 +1,117 @@ +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../../index'; + +describe('app.use(fn)', () => { + it('should compose middleware', async () => { + const app = new Koa(); + const calls: number[] = []; + + app.use((_ctx, next) => { + calls.push(1); + return next().then(() => { + calls.push(6); + }); + }); + + app.use((_ctx, next) => { + calls.push(2); + return next().then(() => { + calls.push(5); + }); + }); + + app.use((_ctx, next) => { + calls.push(3); + return next().then(() => { + calls.push(4); + }); + }); + + const server = app.listen(); + + await request(server) + .get('/') + .expect(404); + + assert.deepStrictEqual(calls, [ 1, 2, 3, 4, 5, 6 ]); + }); + + it('should compose mixed middleware', async () => { + const app = new Koa(); + const calls: number[] = []; + + app.use((_ctx, next) => { + calls.push(1); + return next().then(() => { + calls.push(6); + }); + }); + + app.use(async (_ctx, next) => { + calls.push(2); + await next(); + calls.push(5); + }); + + app.use((_ctx, next) => { + calls.push(3); + return next().then(() => { + calls.push(4); + }); + }); + + const server = app.listen(); + + await request(server) + .get('/') + .expect(404); + + assert.deepStrictEqual(calls, [ 1, 2, 3, 4, 5, 6 ]); + }); + + // https://github.com/koajs/koa/pull/530#issuecomment-148138051 + it('should catch thrown errors in non-async functions', () => { + const app = new Koa(); + + app.use(ctx => ctx.throw('Not Found', 404)); + + return request(app.callback()) + .get('/') + .expect(404); + }); + + it('should throw error on generator middleware', () => { + const app = new Koa(); + + app.use((_ctx, next) => next()); + assert.throws(() => { + app.use((function* generatorMiddileware(next) { + console.log('pre generator'); + yield next; + // this.body = 'generator'; + console.log('post generator'); + }) as any); + }, (err: TypeError) => { + assert.match(err.message, /Support for generators was removed/); + return true; + }); + }); + + it('should throw error for non-function', () => { + const app = new Koa(); + + [ null, undefined, 0, false, 'not a function' ].forEach(v => { + assert.throws(() => app.use(v as any), /middleware must be a function!/); + }); + }); + + it('should remove generator functions support', () => { + const app = new Koa(); + assert.throws(() => { + app.use((function* () { + // empty + }) as any); + }, /Support for generators was removed/); + }); +}); diff --git a/__tests__/context/assert.js b/test/context/assert.test.ts similarity index 69% rename from __tests__/context/assert.js rename to test/context/assert.test.ts index e3eff7dbb..99428540b 100644 --- a/__tests__/context/assert.js +++ b/test/context/assert.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const context = require('../../test-helpers/context'); -const assert = require('assert'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.assert(value, status)', () => { it('should throw an error', () => { @@ -11,7 +8,7 @@ describe('ctx.assert(value, status)', () => { try { ctx.assert(false, 404); throw new Error('asdf'); - } catch (err) { + } catch (err: any) { assert.strictEqual(err.status, 404); assert.strictEqual(err.expose, true); } diff --git a/__tests__/context/cookies.js b/test/context/cookies.test.ts similarity index 80% rename from __tests__/context/cookies.js rename to test/context/cookies.test.ts index 23e949758..fb4d35341 100644 --- a/__tests__/context/cookies.js +++ b/test/context/cookies.test.ts @@ -1,16 +1,13 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('supertest'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; describe('ctx.cookies', () => { describe('ctx.cookies.set()', () => { - it('should set an unsigned cookie', async() => { + it('should set an unsigned cookie', async () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.cookies.set('name', 'jon'); ctx.status = 204; }); @@ -30,10 +27,10 @@ describe('ctx.cookies', () => { it('should error', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { try { ctx.cookies.set('foo', 'bar', { signed: true }); - } catch (err) { + } catch (err: any) { ctx.body = err.message; } }); @@ -44,12 +41,12 @@ describe('ctx.cookies', () => { }); }); - it('should send a signed cookie', async() => { + it('should send a signed cookie', async () => { const app = new Koa(); - app.keys = ['a', 'b']; + app.keys = [ 'a', 'b' ]; - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.cookies.set('name', 'jon', { signed: true }); ctx.status = 204; }); @@ -68,11 +65,11 @@ describe('ctx.cookies', () => { }); describe('with secure', () => { - it('should get secure from request', async() => { + it('should get secure from request', async () => { const app = new Koa(); app.proxy = true; - app.keys = ['a', 'b']; + app.keys = [ 'a', 'b' ]; app.use(ctx => { ctx.cookies.set('name', 'jon', { signed: true }); @@ -95,14 +92,14 @@ describe('ctx.cookies', () => { }); describe('ctx.cookies=', () => { - it('should override cookie work', async() => { + it('should override cookie work', async () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.cookies = { - set(key, value){ + set(key, value) { ctx.set(key, value); - } + }, }; ctx.cookies.set('name', 'jon'); ctx.status = 204; diff --git a/test/context/inspect.test.ts b/test/context/inspect.test.ts new file mode 100644 index 000000000..e4085f7bb --- /dev/null +++ b/test/context/inspect.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert'; +import util from 'node:util'; +import context from '../test-helpers/context'; + +describe('ctx.inspect()', () => { + it('should return a json representation', () => { + const ctx = context(); + const toJSON = ctx.toJSON(); + + assert.deepStrictEqual(toJSON, ctx.inspect()); + assert.deepStrictEqual(util.inspect(toJSON), util.inspect(ctx)); + }); +}); diff --git a/__tests__/context/onerror.js b/test/context/onerror.test.ts similarity index 79% rename from __tests__/context/onerror.js rename to test/context/onerror.test.ts index ea03ad0d0..c064b6e3d 100644 --- a/__tests__/context/onerror.js +++ b/test/context/onerror.test.ts @@ -1,15 +1,14 @@ -'use strict'; - -const assert = require('assert'); -const request = require('supertest'); -const Koa = require('../..'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import request from 'supertest'; +import { runInNewContext } from 'node:vm'; +import Koa from '../..'; +import context from '../test-helpers/context'; describe('ctx.onerror(err)', () => { it('should respond', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; ctx.throw(418, 'boom'); @@ -24,10 +23,10 @@ describe('ctx.onerror(err)', () => { .expect('Content-Length', '4'); }); - it('should unset all headers', async() => { + it('should unset all headers', async () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.set('Vary', 'Accept-Encoding'); ctx.set('X-CSRF-Token', 'asdf'); ctx.body = 'response'; @@ -47,10 +46,10 @@ describe('ctx.onerror(err)', () => { assert.strictEqual(res.headers.hasOwnProperty('x-csrf-token'), false); }); - it('should set headers specified in the error', async() => { + it('should set headers specified in the error', async () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.set('Vary', 'Accept-Encoding'); ctx.set('X-CSRF-Token', 'asdf'); ctx.body = 'response'; @@ -59,8 +58,8 @@ describe('ctx.onerror(err)', () => { status: 418, expose: true, headers: { - 'X-New-Header': 'Value' - } + 'X-New-Header': 'Value', + }, }); }); @@ -76,7 +75,7 @@ describe('ctx.onerror(err)', () => { assert.strictEqual(res.headers.hasOwnProperty('x-csrf-token'), false); }); - it('should ignore error after headerSent', done => { + it.skip('should ignore error after headerSent', done => { const app = new Koa(); app.on('error', err => { @@ -85,7 +84,7 @@ describe('ctx.onerror(err)', () => { done(); }); - app.use(async ctx => { + app.use(async (ctx: any) => { ctx.status = 200; ctx.set('X-Foo', 'Bar'); ctx.flushHeaders(); @@ -96,16 +95,16 @@ describe('ctx.onerror(err)', () => { request(app.callback()) .get('/') .expect('X-Foo', 'Bar') - .expect(200, () => {}); + .expect(200); }); it('should set status specified in the error using statusCode', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; const err = new Error('Not found'); - err.statusCode = 404; + (err as any).statusCode = 404; throw err; }); @@ -123,10 +122,10 @@ describe('ctx.onerror(err)', () => { it('should respond 500', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; const err = new Error('some error'); - err.statusCode = 'notnumber'; + (err as any).statusCode = 'notnumber'; throw err; }); @@ -146,10 +145,10 @@ describe('ctx.onerror(err)', () => { it('should respond 500', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; const err = new Error('some error'); - err.status = 'notnumber'; + (err as any).status = 'notnumber'; throw err; }); @@ -166,10 +165,10 @@ describe('ctx.onerror(err)', () => { it('should respond 404', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; const err = new Error('test for ENOENT'); - err.code = 'ENOENT'; + (err as any).code = 'ENOENT'; throw err; }); @@ -186,10 +185,10 @@ describe('ctx.onerror(err)', () => { it('should respond 500', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'something else'; const err = new Error('some error'); - err.status = 9999; + (err as any).status = 9999; throw err; }); @@ -205,21 +204,20 @@ describe('ctx.onerror(err)', () => { }); describe('when error from another scope thrown', () => { - it('should handle it like a normal error', async() => { - const ExternError = require('vm').runInNewContext('Error'); - + it('should handle it like a normal error', async () => { + const ExternError = runInNewContext('Error'); const app = new Koa(); const error = Object.assign(new ExternError('boom'), { status: 418, - expose: true + expose: true, }); - app.use((ctx, next) => { + app.use(() => { throw error; }); const server = app.listen(); - const gotRightErrorPromise = new Promise((resolve, reject) => { + const gotRightErrorPromise = new Promise((resolve, reject) => { app.on('error', receivedError => { try { assert.strictEqual(receivedError, error); @@ -242,7 +240,7 @@ describe('ctx.onerror(err)', () => { it('should respond with non-error thrown message', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use(() => { throw 'string error'; // eslint-disable-line no-throw-literal }); @@ -259,13 +257,19 @@ describe('ctx.onerror(err)', () => { let removed = 0; const ctx = context(); - ctx.app.emit = () => {}; + (ctx.app as any).emit = () => { + // ignore + }; ctx.res = { - getHeaderNames: () => ['content-type', 'content-length'], + getHeaderNames: () => [ 'content-type', 'content-length' ], removeHeader: () => removed++, - end: () => {}, - emit: () => {} - }; + end: () => { + // ignore + }, + emit: () => { + // ignore + }, + } as any; ctx.onerror(new Error('error')); @@ -280,14 +284,16 @@ describe('ctx.onerror(err)', () => { done(); }); - app.use(async ctx => { + app.use(async () => { throw { key: 'value' }; // eslint-disable-line no-throw-literal }); request(app.callback()) .get('/') .expect(500) - .expect('Internal Server Error', () => {}); + .expect('Internal Server Error', () => { + // ignore + }); }); }); }); diff --git a/__tests__/context/state.js b/test/context/state.test.ts similarity index 71% rename from __tests__/context/state.js rename to test/context/state.test.ts index 422f25cf9..535594188 100644 --- a/__tests__/context/state.js +++ b/test/context/state.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import Koa from '../..'; describe('ctx.state', () => { it('should provide a ctx.state namespace', () => { diff --git a/__tests__/context/throw.js b/test/context/throw.test.ts similarity index 96% rename from __tests__/context/throw.js rename to test/context/throw.test.ts index 39c7a4c8a..d9364a9ea 100644 --- a/__tests__/context/throw.js +++ b/test/context/throw.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const context = require('../../test-helpers/context'); -const assert = require('assert'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.throw(msg)', () => { it('should set .status to 500', () => { @@ -109,7 +106,7 @@ describe('ctx.throw(status)', () => { try { const err = new Error('some error'); - err.status = -1; + (err as any).status = -1; ctx.throw(err); } catch (err) { assert.strictEqual(err.message, 'some error'); @@ -140,7 +137,7 @@ describe('ctx.throw(status, msg, props)', () => { try { ctx.throw(400, 'msg', { prop: true, - status: -1 + status: -1, }); } catch (err) { assert.strictEqual(err.message, 'msg'); diff --git a/__tests__/context/toJSON.js b/test/context/toJSON.test.ts similarity index 77% rename from __tests__/context/toJSON.js rename to test/context/toJSON.test.ts index c8e73154d..3faaf08e7 100644 --- a/__tests__/context/toJSON.js +++ b/test/context/toJSON.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.toJSON()', () => { it('should return a json representation', () => { @@ -22,8 +19,8 @@ describe('ctx.toJSON()', () => { method: 'POST', url: '/items', header: { - 'content-type': 'text/plain' - } + 'content-type': 'text/plain', + }, }, req); assert.deepStrictEqual({ @@ -31,8 +28,8 @@ describe('ctx.toJSON()', () => { message: 'OK', header: { 'content-type': 'text/html; charset=utf-8', - 'content-length': '10' - } + 'content-length': '10', + }, }, res); }); }); diff --git a/__tests__/request/accept.js b/test/request/accept.test.ts similarity index 61% rename from __tests__/request/accept.js rename to test/request/accept.test.ts index ef99349aa..693357227 100644 --- a/__tests__/request/accept.js +++ b/test/request/accept.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const Accept = require('accepts'); -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import Accept from 'accepts'; +import context, { request as Request } from '../test-helpers/context'; describe('ctx.accept', () => { it('should return an Accept instance', () => { @@ -17,11 +14,11 @@ describe('ctx.accept=', () => { it('should replace the accept object', () => { const ctx = context(); ctx.req.headers.accept = 'text/plain'; - assert.deepStrictEqual(ctx.accepts(), ['text/plain']); + assert.deepStrictEqual(ctx.accepts(), [ 'text/plain' ]); - const request = context.request(); + const request = Request(); request.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain'; ctx.accept = Accept(request.req); - assert.deepStrictEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']); + assert.deepStrictEqual(ctx.accepts(), [ 'text/html', 'text/plain', 'image/jpeg', 'application/*' ]); }); }); diff --git a/__tests__/request/accepts.js b/test/request/accepts.test.ts similarity index 90% rename from __tests__/request/accepts.js rename to test/request/accepts.test.ts index a3876a318..79931f1b1 100644 --- a/__tests__/request/accepts.js +++ b/test/request/accepts.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.accepts(types)', () => { describe('with no arguments', () => { @@ -10,7 +7,7 @@ describe('ctx.accepts(types)', () => { it('should return all accepted types', () => { const ctx = context(); ctx.req.headers.accept = 'application/*;q=0.2, image/jpeg;q=0.8, text/html, text/plain'; - assert.deepStrictEqual(ctx.accepts(), ['text/html', 'text/plain', 'image/jpeg', 'application/*']); + assert.deepStrictEqual(ctx.accepts(), [ 'text/html', 'text/plain', 'image/jpeg', 'application/*' ]); }); }); }); @@ -48,8 +45,8 @@ describe('ctx.accepts(types)', () => { it('should return the first match', () => { const ctx = context(); ctx.req.headers.accept = 'text/plain, text/html'; - assert.strictEqual(ctx.accepts(['png', 'text', 'html']), 'text'); - assert.strictEqual(ctx.accepts(['png', 'html']), 'html'); + assert.strictEqual(ctx.accepts([ 'png', 'text', 'html' ]), 'text'); + assert.strictEqual(ctx.accepts([ 'png', 'html' ]), 'html'); }); }); diff --git a/__tests__/request/acceptsCharsets.js b/test/request/acceptsCharsets.test.ts similarity index 84% rename from __tests__/request/acceptsCharsets.js rename to test/request/acceptsCharsets.test.ts index 3211376be..6f1b6c9c3 100644 --- a/__tests__/request/acceptsCharsets.js +++ b/test/request/acceptsCharsets.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.acceptsCharsets()', () => { describe('with no arguments', () => { @@ -10,7 +7,7 @@ describe('ctx.acceptsCharsets()', () => { it('should return accepted types', () => { const ctx = context(); ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5'; - assert.deepStrictEqual(ctx.acceptsCharsets(), ['utf-8', 'utf-7', 'iso-8859-1']); + assert.deepStrictEqual(ctx.acceptsCharsets(), [ 'utf-8', 'utf-7', 'iso-8859-1' ]); }); }); }); @@ -46,7 +43,7 @@ describe('ctx.acceptsCharsets()', () => { it('should return the best fit', () => { const ctx = context(); ctx.req.headers['accept-charset'] = 'utf-8, iso-8859-1;q=0.2, utf-7;q=0.5'; - assert.strictEqual(ctx.acceptsCharsets(['utf-7', 'utf-8']), 'utf-8'); + assert.strictEqual(ctx.acceptsCharsets([ 'utf-7', 'utf-8' ]), 'utf-8'); }); }); }); diff --git a/__tests__/request/acceptsEncodings.js b/test/request/acceptsEncodings.test.ts similarity index 77% rename from __tests__/request/acceptsEncodings.js rename to test/request/acceptsEncodings.test.ts index 2c78b2ffe..c37ced0cd 100644 --- a/__tests__/request/acceptsEncodings.js +++ b/test/request/acceptsEncodings.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.acceptsEncodings()', () => { describe('with no arguments', () => { @@ -10,7 +7,7 @@ describe('ctx.acceptsEncodings()', () => { it('should return accepted types', () => { const ctx = context(); ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2'; - assert.deepStrictEqual(ctx.acceptsEncodings(), ['gzip', 'compress', 'identity']); + assert.deepStrictEqual(ctx.acceptsEncodings(), [ 'gzip', 'compress', 'identity' ]); assert.strictEqual(ctx.acceptsEncodings('gzip', 'compress'), 'gzip'); }); }); @@ -18,7 +15,7 @@ describe('ctx.acceptsEncodings()', () => { describe('when Accept-Encoding is not populated', () => { it('should return identity', () => { const ctx = context(); - assert.deepStrictEqual(ctx.acceptsEncodings(), ['identity']); + assert.deepStrictEqual(ctx.acceptsEncodings(), [ 'identity' ]); assert.strictEqual(ctx.acceptsEncodings('gzip', 'deflate', 'identity'), 'identity'); }); }); @@ -37,7 +34,7 @@ describe('ctx.acceptsEncodings()', () => { it('should return the best fit', () => { const ctx = context(); ctx.req.headers['accept-encoding'] = 'gzip, compress;q=0.2'; - assert.strictEqual(ctx.acceptsEncodings(['compress', 'gzip']), 'gzip'); + assert.strictEqual(ctx.acceptsEncodings([ 'compress', 'gzip' ]), 'gzip'); }); }); }); diff --git a/__tests__/request/acceptsLanguages.js b/test/request/acceptsLanguages.test.ts similarity index 85% rename from __tests__/request/acceptsLanguages.js rename to test/request/acceptsLanguages.test.ts index 8a5641c7d..a43b0dda6 100644 --- a/__tests__/request/acceptsLanguages.js +++ b/test/request/acceptsLanguages.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.acceptsLanguages(langs)', () => { describe('with no arguments', () => { @@ -10,7 +7,7 @@ describe('ctx.acceptsLanguages(langs)', () => { it('should return accepted types', () => { const ctx = context(); ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt'; - assert.deepStrictEqual(ctx.acceptsLanguages(), ['es', 'pt', 'en']); + assert.deepStrictEqual(ctx.acceptsLanguages(), [ 'es', 'pt', 'en' ]); }); }); }); @@ -46,7 +43,7 @@ describe('ctx.acceptsLanguages(langs)', () => { it('should return the best fit', () => { const ctx = context(); ctx.req.headers['accept-language'] = 'en;q=0.8, es, pt'; - assert.strictEqual(ctx.acceptsLanguages(['es', 'en']), 'es'); + assert.strictEqual(ctx.acceptsLanguages([ 'es', 'en' ]), 'es'); }); }); }); diff --git a/__tests__/request/charset.js b/test/request/charset.test.ts similarity index 81% rename from __tests__/request/charset.js rename to test/request/charset.test.ts index f7366bf83..33be4270d 100644 --- a/__tests__/request/charset.js +++ b/test/request/charset.test.ts @@ -1,14 +1,11 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.charset', () => { describe('with no content-type present', () => { it('should return ""', () => { const req = request(); - assert('' === req.charset); + assert(req.charset === ''); }); }); @@ -16,7 +13,7 @@ describe('req.charset', () => { it('should return ""', () => { const req = request(); req.header['content-type'] = 'text/plain'; - assert('' === req.charset); + assert(req.charset === ''); }); }); diff --git a/__tests__/request/fresh.js b/test/request/fresh.test.ts similarity index 92% rename from __tests__/request/fresh.js rename to test/request/fresh.test.ts index 4a6631b3a..b39127e72 100644 --- a/__tests__/request/fresh.js +++ b/test/request/fresh.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.fresh', () => { describe('the request method is not GET and HEAD', () => { diff --git a/__tests__/request/get.js b/test/request/get.test.ts similarity index 83% rename from __tests__/request/get.js rename to test/request/get.test.ts index 2f1f6c14b..23f37cfcf 100644 --- a/__tests__/request/get.js +++ b/test/request/get.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.get(name)', () => { it('should return the field value', () => { diff --git a/__tests__/request/header.js b/test/request/header.test.ts similarity index 78% rename from __tests__/request/header.js rename to test/request/header.test.ts index f495da243..b140d436f 100644 --- a/__tests__/request/header.js +++ b/test/request/header.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.header', () => { it('should return the request header object', () => { diff --git a/__tests__/request/headers.js b/test/request/headers.test.ts similarity index 78% rename from __tests__/request/headers.js rename to test/request/headers.test.ts index 160ac8573..501edefb3 100644 --- a/__tests__/request/headers.js +++ b/test/request/headers.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.headers', () => { it('should return the request header object', () => { diff --git a/__tests__/request/host.js b/test/request/host.test.ts similarity index 91% rename from __tests__/request/host.js rename to test/request/host.test.ts index c64b4af9a..8150aaa6b 100644 --- a/__tests__/request/host.js +++ b/test/request/host.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.host', () => { it('should return host with port', () => { @@ -22,7 +19,7 @@ describe('req.host', () => { it('should not use :authority header', () => { const req = request({ httpVersionMajor: 1, - httpVersion: '1.1' + httpVersion: '1.1', }); req.header[':authority'] = 'foo.com:3000'; req.header.host = 'bar.com:8000'; @@ -34,7 +31,7 @@ describe('req.host', () => { it('should use :authority header', () => { const req = request({ httpVersionMajor: 2, - httpVersion: '2.0' + httpVersion: '2.0', }); req.header[':authority'] = 'foo.com:3000'; req.header.host = 'bar.com:8000'; @@ -44,7 +41,7 @@ describe('req.host', () => { it('should use host header as fallback', () => { const req = request({ httpVersionMajor: 2, - httpVersion: '2.0' + httpVersion: '2.0', }); req.header.host = 'bar.com:8000'; assert.strictEqual(req.host, 'bar.com:8000'); @@ -63,7 +60,7 @@ describe('req.host', () => { it('should be ignored on HTTP/2', () => { const req = request({ httpVersionMajor: 2, - httpVersion: '2.0' + httpVersion: '2.0', }); req.header['x-forwarded-host'] = 'proxy.com:8080'; req.header[':authority'] = 'foo.com:3000'; @@ -84,7 +81,7 @@ describe('req.host', () => { it('should be used on HTTP/2', () => { const req = request({ httpVersionMajor: 2, - httpVersion: '2.0' + httpVersion: '2.0', }); req.app.proxy = true; req.header['x-forwarded-host'] = 'proxy.com:8080'; diff --git a/__tests__/request/hostname.js b/test/request/hostname.test.ts similarity index 94% rename from __tests__/request/hostname.js rename to test/request/hostname.test.ts index 635cdbbfe..d7cff1226 100644 --- a/__tests__/request/hostname.js +++ b/test/request/hostname.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.hostname', () => { it('should return hostname void of port', () => { diff --git a/__tests__/request/href.js b/test/request/href.test.ts similarity index 66% rename from __tests__/request/href.js rename to test/request/href.test.ts index b5be9c823..262e65b3a 100644 --- a/__tests__/request/href.js +++ b/test/request/href.test.ts @@ -1,11 +1,9 @@ - -'use strict'; - -const assert = require('assert'); -const Stream = require('stream'); -const http = require('http'); -const Koa = require('../../'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import Stream from 'node:stream'; +import http from 'node:http'; +import type { AddressInfo } from 'node:net'; +import Koa from '../../'; +import context from '../test-helpers/context'; describe('ctx.href', () => { it('should return the full request url', () => { @@ -13,10 +11,10 @@ describe('ctx.href', () => { const req = { url: '/users/1?next=/dashboard', headers: { - host: 'localhost' + host: 'localhost', }, - socket: socket, - __proto__: Stream.Readable.prototype + socket, + __proto__: Stream.Readable.prototype, }; const ctx = context(req); assert.strictEqual(ctx.href, 'http://localhost/users/1?next=/dashboard'); @@ -30,17 +28,19 @@ describe('ctx.href', () => { app.use(ctx => { ctx.body = ctx.href; }); - app.listen(function(){ - const address = this.address(); + const server = app.listen(() => { + const address = server.address() as AddressInfo; http.get({ host: 'localhost', path: 'http://example.com/foo', - port: address.port + port: address.port, }, res => { assert.strictEqual(res.statusCode, 200); let buf = ''; res.setEncoding('utf8'); - res.on('data', s => buf += s); + res.on('data', s => { + buf += s; + }); res.on('end', () => { assert.strictEqual(buf, 'http://example.com/foo'); done(); diff --git a/__tests__/request/idempotent.js b/test/request/idempotent.test.ts similarity index 69% rename from __tests__/request/idempotent.js rename to test/request/idempotent.test.ts index 98e9875c5..386d845c7 100644 --- a/__tests__/request/idempotent.js +++ b/test/request/idempotent.test.ts @@ -1,14 +1,11 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('ctx.idempotent', () => { describe('when the request method is idempotent', () => { it('should return true', () => { - ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'].forEach(check); - function check(method){ + [ 'GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE' ].forEach(check); + function check(method: string) { const req = request(); req.method = method; assert.strictEqual(req.idempotent, true); diff --git a/test/request/inspect.test.ts b/test/request/inspect.test.ts new file mode 100644 index 000000000..5f5f787ca --- /dev/null +++ b/test/request/inspect.test.ts @@ -0,0 +1,33 @@ +import assert from 'node:assert'; +import util from 'node:util'; +import context from '../test-helpers/context'; + +describe('req.inspect()', () => { + describe('with no request.req present', () => { + it('should return null', () => { + const request = context().request; + request.method = 'GET'; + delete (request as any).req; + assert(undefined === request.inspect()); + assert(util.inspect(request) === 'undefined'); + }); + }); + + it('should return a json representation', () => { + const request = context().request; + request.method = 'GET'; + request.url = 'example.com'; + request.header.host = 'example.com'; + + const expected = { + method: 'GET', + url: 'example.com', + header: { + host: 'example.com', + }, + }; + + assert.deepStrictEqual(request.inspect(), expected); + assert.deepStrictEqual(util.inspect(request), util.inspect(expected)); + }); +}); diff --git a/__tests__/request/ip.js b/test/request/ip.test.ts similarity index 76% rename from __tests__/request/ip.js rename to test/request/ip.test.ts index 8a16fb821..fd2ecd0ce 100644 --- a/__tests__/request/ip.js +++ b/test/request/ip.test.ts @@ -1,10 +1,7 @@ - -'use strict'; - -const assert = require('assert'); -const Stream = require('stream'); -const Koa = require('../..'); -const Request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import Stream from 'node:stream'; +import Koa from '../..'; +import { request as Request } from '../test-helpers/context'; describe('req.ip', () => { describe('with req.ips present', () => { @@ -13,7 +10,7 @@ describe('req.ip', () => { const req = { headers: {}, socket: new Stream.Duplex() }; app.proxy = true; req.headers['x-forwarded-for'] = '127.0.0.1'; - req.socket.remoteAddress = '127.0.0.2'; + (req.socket as any).remoteAddress = '127.0.0.2'; const request = Request(req, undefined, app); assert.strictEqual(request.ip, '127.0.0.1'); }); @@ -22,7 +19,7 @@ describe('req.ip', () => { describe('with no req.ips present', () => { it('should return req.socket.remoteAddress', () => { const req = { socket: new Stream.Duplex() }; - req.socket.remoteAddress = '127.0.0.2'; + (req.socket as any).remoteAddress = '127.0.0.2'; const request = Request(req); assert.strictEqual(request.ip, '127.0.0.2'); }); @@ -32,7 +29,9 @@ describe('req.ip', () => { const socket = new Stream.Duplex(); Object.defineProperty(socket, 'remoteAddress', { get: () => undefined, // So that the helper doesn't override it with a reasonable value - set: () => {} + set: () => { + // empty + }, }); assert.strictEqual(Request({ socket }).ip, ''); }); @@ -41,16 +40,16 @@ describe('req.ip', () => { it('should be lazy inited and cached', () => { const req = { socket: new Stream.Duplex() }; - req.socket.remoteAddress = '127.0.0.2'; + (req.socket as any).remoteAddress = '127.0.0.2'; const request = Request(req); assert.strictEqual(request.ip, '127.0.0.2'); - req.socket.remoteAddress = '127.0.0.1'; + (req.socket as any).remoteAddress = '127.0.0.1'; assert.strictEqual(request.ip, '127.0.0.2'); }); it('should reset ip work', () => { const req = { socket: new Stream.Duplex() }; - req.socket.remoteAddress = '127.0.0.2'; + (req.socket as any).remoteAddress = '127.0.0.2'; const request = Request(req); assert.strictEqual(request.ip, '127.0.0.2'); request.ip = '127.0.0.1'; diff --git a/__tests__/request/ips.js b/test/request/ips.test.ts similarity index 85% rename from __tests__/request/ips.js rename to test/request/ips.test.ts index 4932262c7..0934c0e52 100644 --- a/__tests__/request/ips.js +++ b/test/request/ips.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.ips', () => { describe('when X-Forwarded-For is present', () => { @@ -20,7 +17,7 @@ describe('req.ips', () => { const req = request(); req.app.proxy = true; req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2'; - assert.deepStrictEqual(req.ips, ['127.0.0.1', '127.0.0.2']); + assert.deepStrictEqual(req.ips, [ '127.0.0.1', '127.0.0.2' ]); }); }); }); @@ -42,7 +39,7 @@ describe('req.ips', () => { req.app.proxy = true; req.app.proxyIpHeader = 'x-client-ip'; req.header['x-client-ip'] = '127.0.0.1,127.0.0.2'; - assert.deepStrictEqual(req.ips, ['127.0.0.1', '127.0.0.2']); + assert.deepStrictEqual(req.ips, [ '127.0.0.1', '127.0.0.2' ]); }); }); }); @@ -64,7 +61,7 @@ describe('req.ips', () => { req.app.proxy = true; req.app.maxIpsCount = 1; req.header['x-forwarded-for'] = '127.0.0.1,127.0.0.2'; - assert.deepStrictEqual(req.ips, ['127.0.0.2']); + assert.deepStrictEqual(req.ips, [ '127.0.0.2' ]); }); }); }); diff --git a/__tests__/request/is.js b/test/request/is.test.ts similarity index 89% rename from __tests__/request/is.js rename to test/request/is.test.ts index 88086b6ca..1188a7807 100644 --- a/__tests__/request/is.js +++ b/test/request/is.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const context = require('../../test-helpers/context'); -const assert = require('assert'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.is(type)', () => { it('should ignore params', () => { @@ -77,10 +74,10 @@ describe('ctx.is(type)', () => { assert.strictEqual(ctx.is('image/*', 'image/png'), 'image/png'); assert.strictEqual(ctx.is('image/png', 'image/*'), 'image/png'); - assert.strictEqual(ctx.is(['text/*', 'image/*']), 'image/png'); - assert.strictEqual(ctx.is(['image/*', 'text/*']), 'image/png'); - assert.strictEqual(ctx.is(['image/*', 'image/png']), 'image/png'); - assert.strictEqual(ctx.is(['image/png', 'image/*']), 'image/png'); + assert.strictEqual(ctx.is([ 'text/*', 'image/*' ]), 'image/png'); + assert.strictEqual(ctx.is([ 'image/*', 'text/*' ]), 'image/png'); + assert.strictEqual(ctx.is([ 'image/*', 'image/png' ]), 'image/png'); + assert.strictEqual(ctx.is([ 'image/png', 'image/*' ]), 'image/png'); assert.strictEqual(ctx.is('jpeg'), false); assert.strictEqual(ctx.is('.jpeg'), false); diff --git a/__tests__/request/length.js b/test/request/length.test.ts similarity index 75% rename from __tests__/request/length.js rename to test/request/length.test.ts index 97ddeb11d..bc0811cb7 100644 --- a/__tests__/request/length.js +++ b/test/request/length.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('ctx.length', () => { it('should return length in content-length', () => { diff --git a/__tests__/request/origin.js b/test/request/origin.test.ts similarity index 65% rename from __tests__/request/origin.js rename to test/request/origin.test.ts index 623df45ae..782f1be36 100644 --- a/__tests__/request/origin.js +++ b/test/request/origin.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const assert = require('assert'); -const Stream = require('stream'); -const context = require('../../test-helpers/context'); +import Stream from 'node:stream'; +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.origin', () => { it('should return the origin of url', () => { @@ -11,10 +8,10 @@ describe('ctx.origin', () => { const req = { url: '/users/1?next=/dashboard', headers: { - host: 'localhost' + host: 'localhost', }, - socket: socket, - __proto__: Stream.Readable.prototype + socket, + __proto__: Stream.Readable.prototype, }; const ctx = context(req); assert.strictEqual(ctx.origin, 'http://localhost'); diff --git a/__tests__/request/path.js b/test/request/path.test.ts similarity index 87% rename from __tests__/request/path.js rename to test/request/path.test.ts index 98eb81cb5..0224ca056 100644 --- a/__tests__/request/path.js +++ b/test/request/path.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); -const parseurl = require('parseurl'); +import assert from 'node:assert'; +import parseurl from 'parseurl'; +import context from '../test-helpers/context'; describe('ctx.path', () => { it('should return the pathname', () => { diff --git a/__tests__/request/protocol.js b/test/request/protocol.test.ts similarity index 80% rename from __tests__/request/protocol.js rename to test/request/protocol.test.ts index 51f170920..8b3c2a01e 100644 --- a/__tests__/request/protocol.js +++ b/test/request/protocol.test.ts @@ -1,14 +1,11 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.protocol', () => { describe('when encrypted', () => { it('should return "https"', () => { const req = request(); - req.req.socket = { encrypted: true }; + (req.req as any).socket = { encrypted: true }; assert.strictEqual(req.protocol, 'https'); }); }); @@ -16,7 +13,7 @@ describe('req.protocol', () => { describe('when unencrypted', () => { it('should return "http"', () => { const req = request(); - req.req.socket = {}; + (req.req as any).socket = {}; assert.strictEqual(req.protocol, 'http'); }); }); @@ -26,7 +23,7 @@ describe('req.protocol', () => { it('should be used', () => { const req = request(); req.app.proxy = true; - req.req.socket = {}; + (req.req as any).socket = {}; req.header['x-forwarded-proto'] = 'https, http'; assert.strictEqual(req.protocol, 'https'); }); @@ -35,7 +32,7 @@ describe('req.protocol', () => { it('should return "http"', () => { const req = request(); req.app.proxy = true; - req.req.socket = {}; + (req.req as any).socket = {}; req.header['x-forwarded-proto'] = ''; assert.strictEqual(req.protocol, 'http'); }); @@ -45,7 +42,7 @@ describe('req.protocol', () => { describe('and proxy is not trusted', () => { it('should not be used', () => { const req = request(); - req.req.socket = {}; + (req.req as any).socket = {}; req.header['x-forwarded-proto'] = 'https, http'; assert.strictEqual(req.protocol, 'http'); }); diff --git a/__tests__/request/query.js b/test/request/query.test.ts similarity index 86% rename from __tests__/request/query.js rename to test/request/query.test.ts index fa94982f2..7b94bf62f 100644 --- a/__tests__/request/query.js +++ b/test/request/query.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.query', () => { describe('when missing', () => { @@ -27,7 +24,7 @@ describe('ctx.query', () => { describe('ctx.query=', () => { it('should stringify and replace the query string and search', () => { const ctx = context({ url: '/store/shoes' }); - ctx.query = { page: 2, color: 'blue' }; + ctx.query = { page: '2', color: 'blue' }; assert.strictEqual(ctx.url, '/store/shoes?page=2&color=blue'); assert.strictEqual(ctx.querystring, 'page=2&color=blue'); assert.strictEqual(ctx.search, '?page=2&color=blue'); @@ -35,7 +32,8 @@ describe('ctx.query=', () => { it('should change .url but not .originalUrl', () => { const ctx = context({ url: '/store/shoes' }); - ctx.query = { page: 2 }; + // ctx.query = { page: 2 }; + ctx.query = { page: '2' }; assert.strictEqual(ctx.url, '/store/shoes?page=2'); assert.strictEqual(ctx.originalUrl, '/store/shoes'); assert.strictEqual(ctx.request.originalUrl, '/store/shoes'); diff --git a/__tests__/request/querystring.js b/test/request/querystring.test.ts similarity index 90% rename from __tests__/request/querystring.js rename to test/request/querystring.test.ts index 51bd5df6c..ecba75afc 100644 --- a/__tests__/request/querystring.js +++ b/test/request/querystring.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); -const parseurl = require('parseurl'); +import assert from 'node:assert'; +import parseurl from 'parseurl'; +import context from '../test-helpers/context'; describe('ctx.querystring', () => { it('should return the querystring', () => { @@ -14,7 +11,7 @@ describe('ctx.querystring', () => { describe('when ctx.req not present', () => { it('should return an empty string', () => { const ctx = context(); - ctx.request.req = null; + (ctx.request as any).req = null; assert.strictEqual(ctx.querystring, ''); }); }); diff --git a/__tests__/request/search.js b/test/request/search.test.ts similarity index 91% rename from __tests__/request/search.js rename to test/request/search.test.ts index a6c9f23d4..c55f5f728 100644 --- a/__tests__/request/search.js +++ b/test/request/search.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.search=', () => { it('should replace the search', () => { diff --git a/__tests__/request/secure.js b/test/request/secure.test.ts similarity index 50% rename from __tests__/request/secure.js rename to test/request/secure.test.ts index a35bb8565..bcbb1b98c 100644 --- a/__tests__/request/secure.js +++ b/test/request/secure.test.ts @@ -1,13 +1,10 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.secure', () => { it('should return true when encrypted', () => { const req = request(); - req.req.socket = { encrypted: true }; + (req.req as any).socket = { encrypted: true }; assert.strictEqual(req.secure, true); }); }); diff --git a/__tests__/request/stale.js b/test/request/stale.test.ts similarity index 75% rename from __tests__/request/stale.js rename to test/request/stale.test.ts index 685f4477e..c79f43c9b 100644 --- a/__tests__/request/stale.js +++ b/test/request/stale.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('req.stale', () => { it('should be the inverse of req.fresh', () => { diff --git a/__tests__/request/subdomains.js b/test/request/subdomains.test.ts similarity index 70% rename from __tests__/request/subdomains.js rename to test/request/subdomains.test.ts index 74ff56cbd..6bb232961 100644 --- a/__tests__/request/subdomains.js +++ b/test/request/subdomains.test.ts @@ -1,18 +1,15 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('../../test-helpers/context').request; +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.subdomains', () => { it('should return subdomain array', () => { const req = request(); req.header.host = 'tobi.ferrets.example.com'; req.app.subdomainOffset = 2; - assert.deepStrictEqual(req.subdomains, ['ferrets', 'tobi']); + assert.deepStrictEqual(req.subdomains, [ 'ferrets', 'tobi' ]); req.app.subdomainOffset = 3; - assert.deepStrictEqual(req.subdomains, ['tobi']); + assert.deepStrictEqual(req.subdomains, [ 'tobi' ]); }); it('should work with no host present', () => { diff --git a/__tests__/request/type.js b/test/request/type.test.ts similarity index 76% rename from __tests__/request/type.js rename to test/request/type.test.ts index d79de1107..ae32382c8 100644 --- a/__tests__/request/type.js +++ b/test/request/type.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.type', () => { it('should return type void of parameters', () => { diff --git a/__tests__/request/whatwg-url.js b/test/request/whatwg-url.test.ts similarity index 82% rename from __tests__/request/whatwg-url.js rename to test/request/whatwg-url.test.ts index abe674215..e413056ca 100644 --- a/__tests__/request/whatwg-url.js +++ b/test/request/whatwg-url.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const request = require('../../test-helpers/context').request; -const assert = require('assert'); +import assert from 'node:assert'; +import { request } from '../test-helpers/context'; describe('req.URL', () => { it('should not throw when host is void', () => { diff --git a/__tests__/response/append.js b/test/response/append.test.ts similarity index 64% rename from __tests__/response/append.js rename to test/response/append.test.ts index ab0bb474f..791c2bdeb 100644 --- a/__tests__/response/append.js +++ b/test/response/append.test.ts @@ -1,23 +1,20 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.append(name, val)', () => { it('should append multiple headers', () => { const ctx = context(); ctx.append('x-foo', 'bar1'); ctx.append('x-foo', 'bar2'); - assert.deepStrictEqual(ctx.response.header['x-foo'], ['bar1', 'bar2']); + assert.deepStrictEqual(ctx.response.header['x-foo'], [ 'bar1', 'bar2' ]); }); it('should accept array of values', () => { const ctx = context(); - ctx.append('Set-Cookie', ['foo=bar', 'fizz=buzz']); + ctx.append('Set-Cookie', [ 'foo=bar', 'fizz=buzz' ]); ctx.append('Set-Cookie', 'hi=again'); - assert.deepStrictEqual(ctx.response.header['set-cookie'], ['foo=bar', 'fizz=buzz', 'hi=again']); + assert.deepStrictEqual(ctx.response.header['set-cookie'], [ 'foo=bar', 'fizz=buzz', 'hi=again' ]); }); it('should get reset by res.set(field, val)', () => { @@ -37,6 +34,6 @@ describe('ctx.append(name, val)', () => { ctx.set('Link', ''); ctx.append('Link', ''); - assert.deepStrictEqual(ctx.response.header.link, ['', '']); + assert.deepStrictEqual(ctx.response.header.link, [ '', '' ]); }); }); diff --git a/__tests__/response/attachment.js b/test/response/attachment.test.ts similarity index 97% rename from __tests__/response/attachment.js rename to test/response/attachment.test.ts index be58d7d8f..c2276b295 100644 --- a/__tests__/response/attachment.js +++ b/test/response/attachment.test.ts @@ -1,10 +1,7 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); -const request = require('supertest'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import context from '../test-helpers/context'; +import Koa from '../..'; describe('ctx.attachment([filename])', () => { describe('when given a filename', () => { @@ -35,7 +32,7 @@ describe('ctx.attachment([filename])', () => { it('should work with http client', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.attachment('path/to/include-no-ascii-char-中文名-ok.json'); ctx.body = { foo: 'bar' }; }); diff --git a/__tests__/response/body.js b/test/response/body.test.ts similarity index 96% rename from __tests__/response/body.js rename to test/response/body.test.ts index 4c957d46e..93acec1b0 100644 --- a/__tests__/response/body.js +++ b/test/response/body.test.ts @@ -1,10 +1,7 @@ - -'use strict'; - -const response = require('../../test-helpers/context').response; -const assert = require('assert'); -const fs = require('fs'); -const Stream = require('stream'); +import fs from 'node:fs'; +import Stream from 'node:stream'; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.body=', () => { describe('when Content-Type is set', () => { diff --git a/__tests__/response/etag.js b/test/response/etag.test.ts similarity index 85% rename from __tests__/response/etag.js rename to test/response/etag.test.ts index 78d6419d8..dc1bd4364 100644 --- a/__tests__/response/etag.js +++ b/test/response/etag.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const response = require('../../test-helpers/context').response; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.etag=', () => { it('should not modify an etag with quotes', () => { diff --git a/__tests__/response/flushHeaders.js b/test/response/flushHeaders.test.ts similarity index 85% rename from __tests__/response/flushHeaders.js rename to test/response/flushHeaders.test.ts index 326dc2a04..15a8ddaab 100644 --- a/__tests__/response/flushHeaders.js +++ b/test/response/flushHeaders.test.ts @@ -1,16 +1,15 @@ - -'use strict'; - -const request = require('supertest'); -const assert = require('assert'); -const Koa = require('../..'); -const http = require('http'); +import assert from 'node:assert'; +import http from 'node:http'; +import { PassThrough } from 'node:stream'; +import type { AddressInfo } from 'node:net'; +import request from 'supertest'; +import Koa from '../..'; describe('ctx.flushHeaders()', () => { it('should set headersSent', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.body = 'Body'; ctx.status = 200; ctx.flushHeaders(); @@ -28,7 +27,7 @@ describe('ctx.flushHeaders()', () => { it('should allow a response afterwards', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.status = 200; ctx.res.setHeader('Content-Type', 'text/plain'); ctx.flushHeaders(); @@ -46,7 +45,7 @@ describe('ctx.flushHeaders()', () => { it('should send the correct status code', () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.status = 401; ctx.res.setHeader('Content-Type', 'text/plain'); ctx.flushHeaders(); @@ -61,10 +60,10 @@ describe('ctx.flushHeaders()', () => { .expect('Body'); }); - it('should ignore set header after flushHeaders', async() => { + it('should ignore set header after flushHeaders', async () => { const app = new Koa(); - app.use((ctx, next) => { + app.use((ctx: any) => { ctx.status = 401; ctx.res.setHeader('Content-Type', 'text/plain'); ctx.flushHeaders(); @@ -85,7 +84,6 @@ describe('ctx.flushHeaders()', () => { }); it('should flush headers first and delay to send data', done => { - const PassThrough = require('stream').PassThrough; const app = new Koa(); app.use(ctx => { @@ -100,13 +98,13 @@ describe('ctx.flushHeaders()', () => { }, 10000); }); - app.listen(function(err){ + const server = app.listen(function(err) { if (err) return done(err); - const port = this.address().port; + const port = (server.address() as AddressInfo).port; http.request({ - port + port, }) .on('response', res => { const onData = () => done(new Error('boom')); @@ -124,7 +122,6 @@ describe('ctx.flushHeaders()', () => { }); it('should catch stream error', done => { - const PassThrough = require('stream').PassThrough; const app = new Koa(); app.once('error', err => { assert(err.message === 'mock error'); @@ -146,6 +143,8 @@ describe('ctx.flushHeaders()', () => { const server = app.listen(); - request(server).get('/').end(); + request(server).get('/').end(() => { + // ignore + }); }); }); diff --git a/__tests__/response/has.js b/test/response/has.test.ts similarity index 81% rename from __tests__/response/has.js rename to test/response/has.test.ts index 9fa38c3ea..2f069aadd 100644 --- a/__tests__/response/has.js +++ b/test/response/has.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.response.has(name)', () => { it('should check a field value, case insensitive way', () => { diff --git a/__tests__/response/header.js b/test/response/header.test.ts similarity index 79% rename from __tests__/response/header.js rename to test/response/header.test.ts index 93964e08a..522c3181b 100644 --- a/__tests__/response/header.js +++ b/test/response/header.test.ts @@ -1,10 +1,7 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('supertest'); -const response = require('../../test-helpers/context').response; -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import { response } from '../test-helpers/context'; +import Koa from '../..'; describe('res.header', () => { it('should return the response header object', () => { @@ -16,12 +13,12 @@ describe('res.header', () => { it('should use res.getHeaders() accessor when available', () => { const res = response(); - res.res._headers = null; + (res.res as any)._headers = null; res.res.getHeaders = () => ({ 'x-foo': 'baz' }); assert.deepStrictEqual(res.header, { 'x-foo': 'baz' }); }); - it('should return the response header object when no mocks are in use', async() => { + it('should return the response header object when no mocks are in use', async () => { const app = new Koa(); let header; @@ -39,7 +36,7 @@ describe('res.header', () => { describe('when res._headers not present', () => { it('should return empty object', () => { const res = response(); - res.res._headers = null; + (res.res as any)._headers = null; assert.deepStrictEqual(res.header, {}); }); }); diff --git a/__tests__/response/headers.js b/test/response/headers.test.ts similarity index 73% rename from __tests__/response/headers.js rename to test/response/headers.test.ts index 3c8223a25..a10867ce5 100644 --- a/__tests__/response/headers.js +++ b/test/response/headers.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const response = require('../../test-helpers/context').response; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.header', () => { it('should return the response header object', () => { @@ -14,7 +11,7 @@ describe('res.header', () => { describe('when res._headers not present', () => { it('should return empty object', () => { const res = response(); - res.res._headers = null; + (res.res as any)._headers = null; assert.deepStrictEqual(res.headers, {}); }); }); diff --git a/__tests__/response/inspect.js b/test/response/inspect.test.ts similarity index 76% rename from __tests__/response/inspect.js rename to test/response/inspect.test.ts index be0613e2d..a212cd31d 100644 --- a/__tests__/response/inspect.js +++ b/test/response/inspect.test.ts @@ -1,16 +1,13 @@ - -'use strict'; - -const response = require('../../test-helpers/context').response; -const assert = require('assert'); -const util = require('util'); +import util from 'node:util'; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.inspect()', () => { describe('with no response.res present', () => { it('should return null', () => { const res = response(); res.body = 'hello'; - delete res.res; + delete (res as any).res; assert.strictEqual(res.inspect(), undefined); assert.strictEqual(util.inspect(res), 'undefined'); }); @@ -25,9 +22,9 @@ describe('res.inspect()', () => { message: 'OK', header: { 'content-type': 'text/plain; charset=utf-8', - 'content-length': '5' + 'content-length': '5', }, - body: 'hello' + body: 'hello', }; assert.deepStrictEqual(res.inspect(), expected); diff --git a/__tests__/response/is.js b/test/response/is.test.ts similarity index 87% rename from __tests__/response/is.js rename to test/response/is.test.ts index c38489efc..786eabafa 100644 --- a/__tests__/response/is.js +++ b/test/response/is.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const context = require('../../test-helpers/context'); -const assert = require('assert'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('response.is(type)', () => { it('should ignore params', () => { @@ -61,10 +58,10 @@ describe('response.is(type)', () => { assert.strictEqual(res.is('image/*', 'image/png'), 'image/png'); assert.strictEqual(res.is('image/png', 'image/*'), 'image/png'); - assert.strictEqual(res.is(['text/*', 'image/*']), 'image/png'); - assert.strictEqual(res.is(['image/*', 'text/*']), 'image/png'); - assert.strictEqual(res.is(['image/*', 'image/png']), 'image/png'); - assert.strictEqual(res.is(['image/png', 'image/*']), 'image/png'); + assert.strictEqual(res.is([ 'text/*', 'image/*' ]), 'image/png'); + assert.strictEqual(res.is([ 'image/*', 'text/*' ]), 'image/png'); + assert.strictEqual(res.is([ 'image/*', 'image/png' ]), 'image/png'); + assert.strictEqual(res.is([ 'image/png', 'image/*' ]), 'image/png'); assert.strictEqual(res.is('jpeg'), false); assert.strictEqual(res.is('.jpeg'), false); diff --git a/__tests__/response/last-modified.js b/test/response/last-modified.test.ts similarity index 89% rename from __tests__/response/last-modified.js rename to test/response/last-modified.test.ts index a38582b88..6632b147a 100644 --- a/__tests__/response/last-modified.js +++ b/test/response/last-modified.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const response = require('../../test-helpers/context').response; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.lastModified', () => { it('should set the header as a UTCString', () => { diff --git a/__tests__/response/length.js b/test/response/length.test.ts similarity index 93% rename from __tests__/response/length.js rename to test/response/length.test.ts index 8b4bfdf36..5e3fe89e4 100644 --- a/__tests__/response/length.js +++ b/test/response/length.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const response = require('../../test-helpers/context').response; -const assert = require('assert'); -const fs = require('fs'); +import fs from 'node:fs'; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.length', () => { describe('when Content-Length is defined', () => { diff --git a/__tests__/response/message.js b/test/response/message.test.ts similarity index 85% rename from __tests__/response/message.js rename to test/response/message.test.ts index fe82a1274..1ea7ced79 100644 --- a/__tests__/response/message.js +++ b/test/response/message.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const response = require('../../test-helpers/context').response; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.message', () => { it('should return the response status message', () => { diff --git a/__tests__/response/redirect.js b/test/response/redirect.test.ts similarity index 95% rename from __tests__/response/redirect.js rename to test/response/redirect.test.ts index 362f743e1..70b08feda 100644 --- a/__tests__/response/redirect.js +++ b/test/response/redirect.test.ts @@ -1,10 +1,7 @@ - -'use strict'; - -const assert = require('assert'); -const request = require('supertest'); -const context = require('../../test-helpers/context'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import context from '../test-helpers/context'; +import Koa from '../..'; describe('ctx.redirect(url)', () => { it('should redirect to the given url', () => { @@ -128,7 +125,7 @@ describe('ctx.redirect(url)', () => { }); }); -function escape(html){ +function escape(html) { return String(html) .replace(/&/g, '&') .replace(/"/g, '"') diff --git a/__tests__/response/remove.js b/test/response/remove.test.ts similarity index 67% rename from __tests__/response/remove.js rename to test/response/remove.test.ts index 72c1a375c..de65a875e 100644 --- a/__tests__/response/remove.js +++ b/test/response/remove.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.remove(name)', () => { it('should remove a field', () => { diff --git a/__tests__/response/set.js b/test/response/set.test.ts similarity index 78% rename from __tests__/response/set.js rename to test/response/set.test.ts index 9ec601915..aa5042197 100644 --- a/__tests__/response/set.js +++ b/test/response/set.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.set(name, val)', () => { it('should set a field value', () => { @@ -25,8 +22,8 @@ describe('ctx.set(name, val)', () => { it('should set a field value of array', () => { const ctx = context(); - ctx.set('x-foo', ['foo', 'bar', 123]); - assert.deepStrictEqual(ctx.response.header['x-foo'], ['foo', 'bar', '123']); + ctx.set('x-foo', [ 'foo', 'bar', 123 ]); + assert.deepStrictEqual(ctx.response.header['x-foo'], [ 'foo', 'bar', '123' ]); }); }); @@ -36,7 +33,7 @@ describe('ctx.set(object)', () => { ctx.set({ foo: '1', - bar: '2' + bar: '2', }); assert.strictEqual(ctx.response.header.foo, '1'); diff --git a/__tests__/response/socket.js b/test/response/socket.test.ts similarity index 55% rename from __tests__/response/socket.js rename to test/response/socket.test.ts index dd6b9eb03..acef45281 100644 --- a/__tests__/response/socket.js +++ b/test/response/socket.test.ts @@ -1,9 +1,6 @@ - -'use strict'; - -const assert = require('assert'); -const response = require('../../test-helpers/context').response; -const Stream = require('stream'); +import Stream from 'node:stream'; +import assert from 'node:assert'; +import { response } from '../test-helpers/context'; describe('res.socket', () => { it('should return the request socket object', () => { diff --git a/__tests__/response/status.js b/test/response/status.test.ts similarity index 79% rename from __tests__/response/status.js rename to test/response/status.test.ts index e3d055e7f..9b9c26b37 100644 --- a/__tests__/response/status.js +++ b/test/response/status.test.ts @@ -1,11 +1,8 @@ - -'use strict'; - -const response = require('../../test-helpers/context').response; -const request = require('supertest'); -const statuses = require('statuses'); -const assert = require('assert'); -const Koa = require('../..'); +import assert from 'node:assert'; +import request from 'supertest'; +import statuses from 'statuses'; +import { response } from '../test-helpers/context'; +import Koa from '../..'; describe('res.status=', () => { describe('when a status code', () => { @@ -30,7 +27,9 @@ describe('res.status=', () => { }); describe('and custom status', () => { - beforeEach(() => statuses['700'] = 'custom status'); + beforeEach(() => { + statuses['700'] = 'custom status'; + }); it('should set the status', () => { const res = response(); @@ -47,7 +46,7 @@ describe('res.status=', () => { it('should not set the status message', () => { const res = response({ httpVersionMajor: 2, - httpVersion: '2.0' + httpVersion: '2.0', }); res.status = 200; assert(!res.res.statusMessage); @@ -57,12 +56,14 @@ describe('res.status=', () => { describe('when a status string', () => { it('should throw', () => { - assert.throws(() => response().status = 'forbidden', /status code must be a number/); + assert.throws(() => { + (response() as any).status = 'forbidden'; + }, /status code must be a number/); }); }); - function strip(status){ - it('should strip content related header fields', async() => { + function strip(status) { + it('should strip content related header fields', async () => { const app = new Koa(); app.use(ctx => { @@ -71,9 +72,9 @@ describe('res.status=', () => { ctx.set('Content-Length', '15'); ctx.set('Transfer-Encoding', 'chunked'); ctx.status = status; - assert(null == ctx.response.header['content-type']); - assert(null == ctx.response.header['content-length']); - assert(null == ctx.response.header['transfer-encoding']); + assert(ctx.response.header['content-type'] == null); + assert(ctx.response.header['content-length'] == null); + assert(ctx.response.header['transfer-encoding'] == null); }); const res = await request(app.callback()) @@ -86,7 +87,7 @@ describe('res.status=', () => { assert.strictEqual(res.text.length, 0); }); - it('should strip content related header fields after status set', async() => { + it('should strip content related header fields after status set', async () => { const app = new Koa(); app.use(ctx => { diff --git a/__tests__/response/type.js b/test/response/type.test.ts similarity index 94% rename from __tests__/response/type.js rename to test/response/type.test.ts index f5f0f6aba..3b45f7e43 100644 --- a/__tests__/response/type.js +++ b/test/response/type.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const context = require('../../test-helpers/context'); -const assert = require('assert'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.type=', () => { describe('with a mime', () => { diff --git a/__tests__/response/vary.js b/test/response/vary.test.ts similarity index 85% rename from __tests__/response/vary.js rename to test/response/vary.test.ts index 3e709a3a9..d4941649f 100644 --- a/__tests__/response/vary.js +++ b/test/response/vary.test.ts @@ -1,8 +1,5 @@ - -'use strict'; - -const assert = require('assert'); -const context = require('../../test-helpers/context'); +import assert from 'node:assert'; +import context from '../test-helpers/context'; describe('ctx.vary(field)', () => { describe('when Vary is not set', () => { @@ -17,6 +14,7 @@ describe('ctx.vary(field)', () => { it('should append', () => { const ctx = context(); ctx.vary('Accept'); + assert.strictEqual(ctx.response.header.vary, 'Accept'); ctx.vary('Accept-Encoding'); assert.strictEqual(ctx.response.header.vary, 'Accept, Accept-Encoding'); }); diff --git a/__tests__/response/writable.js b/test/response/writable.test.ts similarity index 90% rename from __tests__/response/writable.js rename to test/response/writable.test.ts index 7d6cf3bbc..92965247d 100644 --- a/__tests__/response/writable.js +++ b/test/response/writable.test.ts @@ -1,17 +1,14 @@ - -'use strict'; - -const assert = require('assert'); -const Koa = require('../../'); -const net = require('net'); +import net from 'node:net'; +import assert from 'node:assert'; +import Koa from '../../'; describe('res.writable', () => { describe('when continuous requests in one persistent connection', () => { - function requestTwice(server, done){ + function requestTwice(server, done) { const port = server.address().port; const buf = Buffer.from('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n'); const client = net.connect(port); - const datas = []; + const datas: Buffer[] = []; client .on('error', done) .on('data', data => datas.push(data)) @@ -40,7 +37,7 @@ describe('res.writable', () => { }); describe('when socket closed before response sent', () => { - function requestClosed(server){ + function requestClosed(server) { const port = server.address().port; const buf = Buffer.from('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n'); const client = net.connect(port); @@ -65,7 +62,7 @@ describe('res.writable', () => { }); describe('when response finished', () => { - function request(server){ + function request(server) { const port = server.address().port; const buf = Buffer.from('GET / HTTP/1.1\r\nHost: localhost:' + port + '\r\nConnection: keep-alive\r\n\r\n'); const client = net.connect(port); @@ -90,6 +87,6 @@ describe('res.writable', () => { }); }); -function sleep(time){ +function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); } diff --git a/test/test-helpers/context.ts b/test/test-helpers/context.ts new file mode 100644 index 000000000..f2e204ae4 --- /dev/null +++ b/test/test-helpers/context.ts @@ -0,0 +1,35 @@ +import stream from 'node:stream'; +import Koa from '../../src/application'; +import type { ContextDelegation } from '../../src/context'; + +export default function context(req?: any, res?: any, app?: Koa) { + const socket = new stream.Duplex(); + req = Object.assign({ headers: {}, socket }, stream.Readable.prototype, req); + res = Object.assign({ _headers: {}, socket }, stream.Writable.prototype, res); + req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1'; + app = app || new Koa(); + res.getHeader = (k: string) => { + return res._headers[k.toLowerCase()]; + }; + res.hasHeader = (k: string) => { + return k.toLowerCase() in res._headers; + }; + res.setHeader = (k: string, v: string | string[]) => { + res._headers[k.toLowerCase()] = v; + }; + res.removeHeader = (k: string) => { + delete res._headers[k.toLowerCase()]; + }; + res.getHeaders = () => { + return res._headers; + }; + return (app as any).createContext(req, res) as ContextDelegation; +} + +export function request(...args: any[]) { + return context(...args).request; +} + +export function response(...args: any[]) { + return context(...args).response; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..1e5143c6a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "outDir": "lib", + "useUnknownInCatchVariables": false + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "test" + ] +}