diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..618ef2b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +test/fixtures +coverage +__snapshots__ diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..63d1994 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + os: 'ubuntu-latest, macos-latest, windows-latest' + version: '18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..035a626 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,13 @@ +name: Release + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: koajs/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.gitignore b/.gitignore index da23d0d..c010914 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,11 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# Deployed apps should consider commenting this line out: -# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git -node_modules +logs/ +npm-debug.log +node_modules/ +coverage/ +test/fixtures/**/run +.DS_Store +.tshy* +.eslintcache +dist +package-lock.json +.package-lock.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ce21122..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -sudo: false -language: node_js -node_js: - - '8' - - '10' -install: - - npm i npminstall && npminstall -script: - - npm run ci -after_script: - - npminstall codecov && codecov 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 a85908c..7e6114a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,16 @@ -koa-onerror -================= +# koa-onerror [![NPM version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] [![Test coverage][codecov-image]][codecov-url] -[![David deps][david-image]][david-url] [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] +[![Node.js Version](https://img.shields.io/node/v/koa-onerror.svg?style=flat)](https://nodejs.org/en/download/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [npm-image]: https://img.shields.io/npm/v/koa-onerror.svg?style=flat [npm-url]: https://npmjs.org/package/koa-onerror -[travis-image]: https://img.shields.io/travis/koajs/onerror.svg?style=flat -[travis-url]: https://travis-ci.org/koajs/onerror [codecov-image]: https://codecov.io/gh/koajs/onerror/branch/master/graph/badge.svg [codecov-url]: https://codecov.io/gh/koajs/onerror -[david-image]: https://img.shields.io/david/koajs/onerror.svg?style=flat -[david-url]: https://david-dm.org/koajs/onerror [snyk-image]: https://snyk.io/test/npm/koa-onerror/badge.svg?style=flat-square [snyk-url]: https://snyk.io/test/npm/koa-onerror [download-image]: https://img.shields.io/npm/dm/koa-onerror.svg?style=flat-square @@ -24,6 +19,7 @@ koa-onerror an error handler for koa, hack ctx.onerror. different with [koa-error](https://github.com/koajs/error): + - we can not just use try catch to handle all errors, steams' and events' errors are directly handle by `ctx.onerror`, so if we want to handle all errors in one place, the only way i can see is to hack `ctx.onerror`. @@ -39,10 +35,10 @@ npm install koa-onerror ```js const fs = require('fs'); -const koa = require('koa'); -const onerror = require('koa-onerror'); +const Koa = require('koa'); +const { onerror } = require('koa-onerror'); -const app = new koa(); +const app = new Koa(); onerror(app); @@ -58,11 +54,11 @@ app.use(ctx => { onerror(app, options); ``` -* **all**: if options.all exist, ignore negotiation -* **text**: text error handler -* **json**: json error handler -* **html**: html error handler -* **redirect**: if accepct html, can redirect to another error page +- **all**: if `options.all` exist, ignore negotiation +- **text**: text error handler +- **json**: json error handler +- **html**: html error handler +- **redirect**: if accept `html` or `text`, can redirect to another error page check out default handler to write your own handler. @@ -76,4 +72,6 @@ check out default handler to write your own handler. ## Contributors -[![](https://ergatejs.implements.io/badges/contributors/koajs/onerror.svg?size=96)](https://github.com/koajs/onerror/graphs/contributors) +[![Contributors](https://contrib.rocks/image?repo=koajs/onerror)](https://github.com/koajs/onerror/graphs/contributors) + +Made with [contributors-img](https://contrib.rocks). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 981e82b..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,15 +0,0 @@ -environment: - matrix: - - nodejs_version: '8' - - nodejs_version: '10' - -install: - - ps: Install-Product node $env:nodejs_version - - npm i npminstall && node_modules\.bin\npminstall - -test_script: - - node --version - - npm --version - - npm run test - -build: off diff --git a/example.cjs b/example.cjs new file mode 100644 index 0000000..e47a81d --- /dev/null +++ b/example.cjs @@ -0,0 +1,15 @@ +const fs = require('node:fs'); +const Koa = require('koa'); +const { onerror } = require('./'); + +const app = new Koa(); + +onerror(app); + +app.use(async ctx => { + foo(); + ctx.body = fs.createReadStream('not exist'); +}); + +app.listen(3000); +console.log('listening on port http://localhost:3000'); diff --git a/example.js b/example.js deleted file mode 100644 index c37f0b6..0000000 --- a/example.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const koa = require('koa'); -const onerror = require('./'); -const app = koa(); - -onerror(app); - -app.use(function* () { - // foo(); - this.body = fs.createReadStream('not exist'); -}); - -app.listen(3000); -console.log('listening on port 3000'); diff --git a/index.js b/index.js deleted file mode 100644 index ee43444..0000000 --- a/index.js +++ /dev/null @@ -1,143 +0,0 @@ -'use strict'; - -const http = require('http'); -const path = require('path'); -const fs = require('fs'); -const escapeHtml = require('escape-html'); -const sendToWormhole = require('stream-wormhole'); - -const env = process.env.NODE_ENV || 'development'; -const isDev = env === 'development'; -const templatePath = isDev - ? path.join(__dirname, 'templates/dev_error.html') - : path.join(__dirname, 'templates/prod_error.html'); -const defaultTemplate = fs.readFileSync(templatePath, 'utf8'); - -const defaultOptions = { - text, - json, - html, - redirect: null, - template: path.join(__dirname, 'error.html'), - accepts: null, -}; - -module.exports = function onerror(app, options) { - options = Object.assign({}, defaultOptions, options); - - app.context.onerror = function(err) { - // don't do anything if there is no error. - // this allows you to pass `this.onerror` - // to node-style callbacks. - if (err == null) return; - - // ignore all pedding request stream - if (this.req) sendToWormhole(this.req); - - // wrap non-error object - if (!(err instanceof Error)) { - let errMsg = err; - if (typeof err === 'object') { - try { - errMsg = JSON.stringify(err); - // eslint-disable-next-line no-empty - } catch (e) {} - } - const newError = new Error('non-error thrown: ' + errMsg); - // err maybe an object, try to copy the name, message and stack to the new error instance - if (err) { - if (err.name) newError.name = err.name; - if (err.message) newError.message = err.message; - if (err.stack) newError.stack = err.stack; - if (err.status) newError.status = err.status; - if (err.headers) newError.headers = err.headers; - } - err = newError; - } - - const headerSent = this.headerSent || !this.writable; - if (headerSent) err.headerSent = true; - - // delegate - this.app.emit('error', err, this); - - // nothing we can do here other - // than delegate to the app-level - // handler and log. - if (headerSent) return; - - // ENOENT support - if (err.code === 'ENOENT') err.status = 404; - - if (typeof err.status !== 'number' || !http.STATUS_CODES[err.status]) { - err.status = 500; - } - this.status = err.status; - - this.set(err.headers); - let type = 'text'; - if (options.accepts) { - type = options.accepts.call(this, 'html', 'text', 'json'); - } else { - type = this.accepts('html', 'text', 'json'); - } - type = type || 'text'; - if (options.all) { - options.all.call(this, err, this); - } else { - if (options.redirect && type !== 'json') { - this.redirect(options.redirect); - } else { - options[type].call(this, err, this); - this.type = type; - } - } - - if (type === 'json') { - this.body = JSON.stringify(this.body); - } - this.res.end(this.body); - }; - - return app; -}; - -/** - * default text error handler - * @param {Error} err - */ - -function text(err, ctx) { - // unset all headers, and set those specified - ctx.res._headers = {}; - ctx.set(err.headers); - - ctx.body = (isDev || err.expose) && err.message - ? err.message - : http.STATUS_CODES[this.status]; -} - -/** - * default json error handler - * @param {Error} err - */ - -function json(err, ctx) { - const message = (isDev || err.expose) && err.message - ? err.message - : http.STATUS_CODES[this.status]; - - ctx.body = { error: message }; -} - -/** - * default html error handler - * @param {Error} err - */ - -function html(err, ctx) { - ctx.body = defaultTemplate - .replace('{{status}}', escapeHtml(err.status)) - .replace('{{stack}}', escapeHtml(err.stack)); - ctx.type = 'html'; -} diff --git a/package.json b/package.json index 19e9991..4ef8d7b 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,6 @@ "name": "koa-onerror", "version": "4.2.0", "description": "koa error handler, hack ctx.onerror", - "main": "index.js", - "scripts": { - "test": "NODE_ENV=development egg-bin test", - "test-cov": "NODE_ENV=development egg-bin cov", - "ci": "npm run lint && npm run test-cov", - "lint": "eslint test *.js --fix" - }, - "files": [ - "index.js", - "templates" - ], "repository": { "type": "git", "url": "git://github.com/koajs/onerror.git" @@ -28,28 +17,71 @@ "url": "https://github.com/koajs/onerror/issues" }, "homepage": "https://github.com/koajs/onerror", + "engines": { + "node": ">= 18.19.0" + }, + "dependencies": { + "escape-html": "^1.0.3", + "stream-wormhole": "^2.0.1" + }, "devDependencies": { - "autod": "*", + "@arethetypeswrong/cli": "^0.17.3", + "@eggjs/bin": "7", + "@eggjs/supertest": "^8.2.0", + "@eggjs/tsconfig": "1", + "@types/escape-html": "^1.0.4", + "@types/koa": "^2.15.0", + "@types/mocha": "10", + "@types/node": "22", "co-busboy": "^1.4.0", - "egg-bin": "^4.3.5", - "egg-ci": "1", - "eslint": "4", - "eslint-config-egg": "5", - "formstream": "^1.1.0", + "eslint": "8", + "eslint-config-egg": "14", + "formstream": "^1.5.1", "koa": "2", - "mz-modules": "^2.1.0", - "pedding": "1", - "supertest": "3", - "urllib": "^2.29.1" + "mm": "^4.0.2", + "rimraf": "6", + "snap-shot-it": "^7.9.10", + "tshy": "3", + "tshy-after": "1", + "typescript": "5", + "urllib": "^4.6.11" }, - "engines": { - "node": ">= 8.0.0" + "scripts": { + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix", + "test": "egg-bin test", + "test:snapshot:update": "SNAPSHOT_UPDATE=1 egg-bin test", + "preci": "npm run clean && npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && npm run clean", + "clean": "rimraf dist", + "prepublishOnly": "tshy && tshy-after && attw --pack" }, - "ci": { - "version": "8, 10" + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } }, - "dependencies": { - "escape-html": "^1.0.3", - "stream-wormhole": "^1.1.0" - } + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..06d3abe --- /dev/null +++ b/src/index.ts @@ -0,0 +1,184 @@ +import http from 'node:http'; +import path from 'node:path'; +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { debuglog, format } from 'node:util'; +import escapeHtml from 'escape-html'; +import { sendToWormhole } from 'stream-wormhole'; + +const debug = debuglog('koa-onerror'); + +export type OnerrorError = Error & { + status: number; + headers?: Record; + expose?: boolean; +}; + +export type OnerrorHandler = (err: OnerrorError, ctx: any) => void; + +export type OnerrorOptions = { + text?: OnerrorHandler; + json?: OnerrorHandler; + html?: OnerrorHandler; + all?: OnerrorHandler; + redirect?: string | null; + accepts?: (...args: string[]) => string; +}; + +const defaultOptions: OnerrorOptions = { + text, + json, + html, +}; + +export function onerror(app: any, options?: OnerrorOptions) { + options = { ...defaultOptions, ...options }; + + app.context.onerror = function(err: any) { + debug('onerror: %s', err); + // don't do anything if there is no error. + // this allows you to pass `this.onerror` + // to node-style callbacks. + if (err == null) { + return; + } + + // ignore all padding request stream + if (this.req) { + sendToWormhole(this.req); + debug('send the req to wormhole'); + } + + // wrap non-error object + if (!(err instanceof Error)) { + debug('err is not an instance of Error'); + let errMsg = err; + if (typeof err === 'object') { + try { + errMsg = JSON.stringify(err); + } catch (e) { + debug('stringify error: %s', e); + errMsg = format('%s', e); + } + } + const newError = new Error('non-error thrown: ' + errMsg); + // err maybe an object, try to copy the name, message and stack to the new error instance + if (err) { + if (err.name) newError.name = err.name; + if (err.message) newError.message = err.message; + if (err.stack) newError.stack = err.stack; + if (err.status) { + Reflect.set(newError, 'status', err.status); + } + if (err.headers) { + Reflect.set(newError, 'headers', err.headers); + } + } + err = newError; + debug('wrap err: %s', err); + } + + const headerSent = this.headerSent || !this.writable; + if (headerSent) { + debug('headerSent is true'); + err.headerSent = true; + } + + // delegate + this.app.emit('error', err, this); + + // nothing we can do here other + // than delegate to the app-level + // handler and log. + if (headerSent) return; + + // ENOENT support + if (err.code === 'ENOENT') { + err.status = 404; + } + + if (typeof err.status !== 'number' || !http.STATUS_CODES[err.status]) { + err.status = 500; + } + this.status = err.status; + + this.set(err.headers); + let type = 'text'; + if (options.accepts) { + type = options.accepts.call(this, 'html', 'text', 'json'); + } else { + type = this.accepts('html', 'text', 'json'); + } + type = type || 'text'; + if (options.all) { + options.all.call(this, err, this); + } else { + if (options.redirect && type !== 'json') { + this.redirect(options.redirect); + } else { + (options as any)[type].call(this, err, this); + this.type = type; + } + } + + if (type === 'json') { + this.body = JSON.stringify(this.body); + } + debug('end the response, body: %s', this.body); + this.res.end(this.body); + }; + + return app; +} + +const devTemplate = fs.readFileSync(path.join(getSourceDirname(), 'templates/dev_error.html'), 'utf8'); +const prodTemplate = fs.readFileSync(path.join(getSourceDirname(), 'templates/prod_error.html'), 'utf8'); + +function isDev() { + return !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; +} + +/** + * default text error handler + */ +function text(err: OnerrorError, ctx: any) { + // unset all headers, and set those specified + ctx.res._headers = {}; + ctx.set(err.headers); + + ctx.body = (isDev() || err.expose) && err.message + ? err.message + : http.STATUS_CODES[ctx.status]; +} + +/** + * default json error handler + */ +function json(err: OnerrorError, ctx: any) { + const message = (isDev() || err.expose) && err.message + ? err.message + : http.STATUS_CODES[ctx.status]; + + ctx.body = { error: message }; +} + +/** + * default html error handler + */ +function html(err: OnerrorError, ctx: any) { + const template = isDev() ? devTemplate : prodTemplate; + ctx.body = template + .replace('{{status}}', escapeHtml(String(err.status))) + .replace('{{stack}}', escapeHtml(err.stack)); + ctx.type = 'html'; +} + +function getSourceDirname() { + if (typeof __dirname === 'string') { + return __dirname; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const __filename = fileURLToPath(import.meta.url); + return path.dirname(__filename); +} diff --git a/templates/dev_error.html b/src/templates/dev_error.html similarity index 100% rename from templates/dev_error.html rename to src/templates/dev_error.html diff --git a/templates/prod_error.html b/src/templates/prod_error.html similarity index 100% rename from templates/prod_error.html rename to src/templates/prod_error.html diff --git a/test/accepts.test.js b/test/accepts.test.js deleted file mode 100644 index f915bf6..0000000 --- a/test/accepts.test.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -const koa = require('koa'); -const request = require('supertest'); -const pedding = require('pedding'); -const onerror = require('..'); - -describe('accepts.test.js', function() { - it('should return json response', function(done) { - done = pedding(2, done); - const app = new koa(); - app.on('error', function() {}); - onerror(app, { - accepts() { - if (this.url.indexOf('.json') > 0) { - return 'json'; - } - return 'text'; - }, - }); - app.use(commonError); - - request(app.callback()) - .get('/user.json') - .set('Accept', '*/*') - .expect(500) - .expect('Content-Type', 'application/json; charset=utf-8') - .expect({ error: 'foo is not defined' }, done); - - request(app.callback()) - .get('/user') - .set('Accept', 'application/json') - .expect(500) - .expect('Content-Type', 'text/plain; charset=utf-8') - .expect('foo is not defined', done); - }); - - it('should redrect when accepts type not json', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app, { - accepts() { - if (this.url.indexOf('.json') > 0) { - return 'json'; - } - return 'text'; - }, - redirect: 'http://foo.com/500.html', - }); - app.use(commonError); - - request(app.callback()) - .get('/user') - .set('Accept', '*/*') - .expect('Content-Type', 'text/html; charset=utf-8') - .expect('Location', 'http://foo.com/500.html') - .expect('Redirecting to http://foo.com/500.html.') - .expect(302, done); - }); -}); - -function commonError() { - // eslint-disable-next-line - foo(); -} diff --git a/test/accepts.test.ts b/test/accepts.test.ts new file mode 100644 index 0000000..1950260 --- /dev/null +++ b/test/accepts.test.ts @@ -0,0 +1,80 @@ +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import { onerror } from '../src/index.js'; + +describe('test/accepts.test.ts', () => { + beforeEach(() => { + mm(process.env, 'NODE_ENV', ''); + }); + + afterEach(() => { + mm.restore(); + }); + + it('should return json response', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app, { + accepts(this: { url: string }) { + if (this.url.includes('.json')) { + return 'json'; + } + return 'text'; + }, + }); + app.use(commonError); + + await request(app.callback()) + .get('/user.json') + .set('Accept', '*/*') + .expect(500) + .expect('Content-Type', 'application/json; charset=utf-8') + .expect({ error: 'foo is not defined' }); + + await request(app.callback()) + .get('/user') + .set('Accept', 'application/json') + .expect(500) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('foo is not defined'); + + // NODE_ENV=production + mm(process.env, 'NODE_ENV', 'production'); + await request(app.callback()) + .get('/user') + .set('Accept', 'application/json') + .expect(500) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect('Internal Server Error'); + }); + + it('should redirect when accepts type not json', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app, { + accepts(this: any) { + if (this.url.indexOf('.json') > 0) { + return 'json'; + } + return 'text'; + }, + redirect: 'http://foo.com/500.html', + }); + app.use(commonError); + + await request(app.callback()) + .get('/user') + .set('Accept', '*/*') + .expect('Content-Type', 'text/html; charset=utf-8') + .expect('Location', 'http://foo.com/500.html') + .expect('Redirecting to http://foo.com/500.html.') + .expect(302); + }); +}); + +function commonError() { + // eslint-disable-next-line + // @ts-ignore - intentionally calling undefined function to trigger error + foo(); +} diff --git a/test/fluid.test.js b/test/fluid.test.js deleted file mode 100644 index a5fb3d1..0000000 --- a/test/fluid.test.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const koa = require('koa'); -const assert = require('assert'); -const onerror = require('..'); - -describe('fluid.test.js', function() { - it('should return app reference', function() { - const app = new koa(); - const res = onerror(app); - assert(res instanceof koa); - assert(res === app); - }); -}); diff --git a/test/fluid.test.ts b/test/fluid.test.ts new file mode 100644 index 0000000..30c7075 --- /dev/null +++ b/test/fluid.test.ts @@ -0,0 +1,12 @@ +import { strict as assert } from 'node:assert'; +import Koa from 'koa'; +import { onerror } from '../src/index.js'; + +describe('test/fluid.test.ts', () => { + it('should return app reference', () => { + const app = new Koa(); + const res = onerror(app); + assert(res instanceof Koa); + assert.equal(res, app); + }); +}); diff --git a/test/form_app.js b/test/form_app.ts similarity index 59% rename from test/form_app.js rename to test/form_app.ts index e404025..30ed088 100644 --- a/test/form_app.js +++ b/test/form_app.ts @@ -1,9 +1,9 @@ -'use strict'; - -const Koa = require('koa'); -const sleep = require('mz-modules/sleep'); -const parse = require('co-busboy'); -const onerror = require('..'); +import { scheduler } from 'node:timers/promises'; +import Koa from 'koa'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore - co-busboy is not typed +import parse from 'co-busboy'; +import { onerror } from '../src/index.js'; const app = new Koa(); app.on('error', () => {}); @@ -22,19 +22,19 @@ app.use(async ctx => { `; return; } - await sleep(10); + await scheduler.wait(10); if (!ctx.is('multipart')) { ctx.throw(400, 'Content-Type must be multipart/*'); } const parts = parse(ctx, { autoFields: true }); const stream = await parts(); - console.log(stream.filename, parts.field); + // console.log(stream.filename, parts.field); stream.undefiend.error(); }); -if (!module.parent) { - app.listen(8080); - console.log('Listen at http://127.0.0.1:8080'); -} +// if (!module.parent) { +// app.listen(8080); +// console.log('Listen at http://127.0.0.1:8080'); +// } -module.exports = app; +export { app }; diff --git a/test/helper.ts b/test/helper.ts new file mode 100644 index 0000000..6c5c063 --- /dev/null +++ b/test/helper.ts @@ -0,0 +1,9 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export function getFixtures(filename: string) { + return path.join(__dirname, 'fixtures', filename); +} diff --git a/test/html.test.js b/test/html.test.js deleted file mode 100644 index e074a72..0000000 --- a/test/html.test.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const koa = require('koa'); -const request = require('supertest'); -const sleep = require('mz-modules/sleep'); -const onerror = require('..'); - -describe('html.test.js', function() { - it('should common error ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/html') - .expect(/

Looks like something broke!<\/p>/, done); - }); - - it('should common error after sleep a little while ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(commonSleepError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/html') - .expect(/

Looks like something broke!<\/p>/, done); - }); - - it('should stream error ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(streamError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/html') - .expect(/

Looks like something broke!<\/p>/) - .expect(/ENOENT/, done); - }); - - it('should unsafe error ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(unsafeError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/html') - .expect(/

Looks like something broke!<\/p>/) - .expect(/<anonymous>/, done); - }); -}); - -function commonError() { - // eslint-disable-next-line - foo(); -} - -async function commonSleepError() { - await sleep(50); - // eslint-disable-next-line - fooAfterSleep(); -} - -function streamError(ctx) { - ctx.body = fs.createReadStream('not exist'); -} - -function unsafeError() { - throw new Error(''); -} diff --git a/test/html.test.ts b/test/html.test.ts new file mode 100644 index 0000000..755daf3 --- /dev/null +++ b/test/html.test.ts @@ -0,0 +1,87 @@ +import fs from 'node:fs'; +import { scheduler } from 'node:timers/promises'; +import Koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import { onerror } from '../src/index.js'; + +describe('test/html.test.ts', () => { + beforeEach(() => { + mm(process.env, 'NODE_ENV', 'development'); + }); + + afterEach(() => { + mm.restore(); + }); + + it('should common error ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/html') + .expect(/

Looks like something broke!<\/p>/); + }); + + it('should common error after sleep a little while ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(commonSleepError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/html') + .expect(/

Looks like something broke!<\/p>/); + }); + + it('should stream error ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(streamError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/html') + .expect(/

Looks like something broke!<\/p>/) + .expect(/ENOENT/); + }); + + it('should unsafe error ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(unsafeError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/html') + .expect(/

Looks like something broke!<\/p>/) + .expect(/<anonymous>/); + }); +}); + +function commonError() { + // eslint-disable-next-line + // @ts-ignore - intentionally calling undefined function to trigger error + foo(); +} + +async function commonSleepError() { + await scheduler.wait(50); + // eslint-disable-next-line + // @ts-ignore - intentionally calling undefined function to trigger error + fooAfterSleep(); +} + +function streamError(ctx: Koa.Context) { + ctx.body = fs.createReadStream('not exist'); +} + +function unsafeError() { + throw new Error(''); +} diff --git a/test/json.test.js b/test/json.test.js deleted file mode 100644 index 8163b79..0000000 --- a/test/json.test.js +++ /dev/null @@ -1,195 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const fs = require('fs'); -const koa = require('koa'); -const request = require('supertest'); -const pedding = require('pedding'); -const onerror = require('..'); - -describe('json.test.js', () => { - it('should common error ok', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ error: 'foo is not defined' }, done); - }); - - it('should stream error ok', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app); - app.use(streamError); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(404, (err, res) => { - assert(!err); - assert(typeof res.body.error === 'string'); - assert(res.body.error.match(/ENOENT/)); - done(); - }); - }); - - it('should custom handler', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app, { - json() { - this.status = 500; - this.body = { - message: 'error', - }; - }, - }); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ message: 'error' }, done); - }); - - it('should show status error when err.message not present', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app); - app.use(emptyError); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ error: 'Internal Server Error' }, done); - }); - - it('should wrap non-error primitive value', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app); - app.use(() => { - throw 1; - }); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ error: 'non-error thrown: 1' }, done); - }); - - it('should wrap non-error object and stringify it', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app); - app.use(() => { - throw { error: true }; - }); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ error: 'non-error thrown: {"error":true}' }, done); - }); - - it('should wrap mock error obj instead of Error instance', done => { - done = pedding(2, done); - const app = new koa(); - app.on('error', err => { - assert(err instanceof Error); - assert(err.name === 'TypeError'); - assert(err.message === 'mock error'); - assert(err.stack.match(/json\.test\.js/)); - done(); - }); - onerror(app); - app.use(() => { - const err = { - name: 'TypeError', - message: 'mock error', - stack: new Error().stack, - status: 404, - headers: { foo: 'bar' }, - }; - throw err; - }); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(404) - .expect('foo', 'bar') - .expect({ error: 'mock error' }, done); - }); - - it('should custom handler with ctx', done => { - const app = new koa(); - app.on('error', () => {}); - onerror(app, { - json: (err, ctx) => { - ctx.status = 500; - ctx.body = { - message: 'error', - }; - }, - }); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ message: 'error' }, done); - }); - - it('should get headerSent in error listener', done => { - const app = new koa(); - app.on('error', err => { - assert(err.headerSent); - done(); - }); - onerror(app, { - json: (err, ctx) => { - ctx.status = 500; - ctx.body = { - message: 'error', - }; - }, - }); - - app.use(ctx => { - ctx.res.flushHeaders(); - throw new Error('mock error'); - }); - - request(app.callback()) - .get('/') - .set('Accept', 'application/json') - .expect(500) - .expect({ message: 'error' }, done); - }); -}); - -function emptyError() { - const err = new Error(''); - err.expose = true; - throw err; -} - -function commonError() { - // eslint-disable-next-line - foo(); -} - -function streamError(ctx) { - ctx.body = fs.createReadStream('not exist'); -} diff --git a/test/json.test.ts b/test/json.test.ts new file mode 100644 index 0000000..ab4f639 --- /dev/null +++ b/test/json.test.ts @@ -0,0 +1,207 @@ +import { strict as assert } from 'node:assert'; +import { once } from 'node:events'; +import fs from 'node:fs'; +import Koa, { Context } from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import { onerror, OnerrorError } from '../src/index.js'; + +describe('test/json.test.ts', () => { + beforeEach(() => { + mm(process.env, 'NODE_ENV', 'development'); + }); + + afterEach(() => { + mm.restore(); + }); + + it('should common error ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ error: 'foo is not defined' }); + }); + + it('should stream error ok', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(streamError); + + const res = await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(404); + assert.equal(typeof res.body.error, 'string'); + assert.match(res.body.error, /ENOENT/); + }); + + it('should custom handler', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app, { + json(this: Context) { + this.status = 500; + this.body = { + message: 'error', + }; + }, + }); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ message: 'error' }); + }); + + it('should show status error when err.message not present', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(emptyError); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ error: 'Internal Server Error' }); + }); + + it('should wrap non-error primitive value', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(() => { + throw 1; + }); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ error: 'non-error thrown: 1' }); + }); + + it('should wrap non-error object and stringify it', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app); + app.use(() => { + throw { error: true }; + }); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ error: 'non-error thrown: {"error":true}' }); + }); + + it('should wrap mock error obj instead of Error instance', async () => { + const app = new Koa(); + onerror(app); + app.use(() => { + const err = { + name: 'TypeError', + message: 'mock error', + stack: new Error().stack, + status: 404, + headers: { foo: 'bar' }, + }; + throw err; + }); + + const errorEvent = once(app, 'error'); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(404) + .expect('foo', 'bar') + .expect({ error: 'mock error' }); + + const [ err ] = await errorEvent; + assert(err instanceof Error); + assert.equal(err.name, 'TypeError'); + assert.equal(err.message, 'mock error'); + assert.match(err.stack!, /json\.test\./); + }); + + it('should custom handler with ctx', async () => { + const app = new Koa(); + app.on('error', () => {}); + onerror(app, { + json: (_err, ctx) => { + ctx.status = 500; + ctx.body = { + message: 'error', + }; + }, + }); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ message: 'error' }); + }); + + it('should get headerSent in error listener', async () => { + const app = new Koa(); + onerror(app, { + json: (_err: OnerrorError, ctx: Context) => { + ctx.status = 500; + ctx.body = { + message: 'error', + }; + }, + }); + + app.use(ctx => { + ctx.res.flushHeaders(); + throw new Error('mock error'); + }); + + const errorEvent = once(app, 'error'); + + request(app.callback()) + .get('/') + .set('Accept', 'application/json') + .expect(500) + .expect({ message: 'error' }) + .send() + .catch(err => { + assert(err instanceof Error); + assert.equal((err as any).headerSent, true); + }); + + const [ err ] = await errorEvent; + assert(err instanceof Error); + assert.equal((err as any).headerSent, true); + }); +}); + +function emptyError() { + const err = new Error('') as OnerrorError; + err.expose = true; + throw err; +} + +function commonError() { + // eslint-disable-next-line + // @ts-ignore foo is not defined + foo(); +} + +function streamError(ctx: Context) { + ctx.body = fs.createReadStream('not exist'); +} diff --git a/test/multipart.test.js b/test/multipart.test.js deleted file mode 100644 index 830e73c..0000000 --- a/test/multipart.test.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -const path = require('path'); -const assert = require('assert'); -const urllib = require('urllib'); -const Agent = require('http').Agent; -const formstream = require('formstream'); -const sleep = require('mz-modules/sleep'); - -describe('multipart.test.js', () => { - let app; - let host; - before(done => { - app = require('./form_app'); - const server = app.listen(0, err => { - host = `http://127.0.0.1:${server.address().port}`; - done(err); - }); - }); - - it('should consume all request data after error throw', async () => { - const keepAliveAgent = new Agent({ - keepAlive: true, - }); - // retry 10 times - for (let i = 0; i < 10; i++) { - const form = formstream(); - form.file('file1', path.join(__dirname, 'fixtures/bigdata.txt')); - form.field('foo', 'fengmk2') - .field('love', 'koa') - .field('index', `${i}`); - - const headers = form.headers(); - const result = await urllib.request(`${host}/upload`, { - method: 'POST', - headers, - stream: form, - timing: true, - agent: keepAliveAgent, - }); - - const data = result.data; - const response = result.res; - if (i === 0) { - assert(response.keepAliveSocket === false); - } else { - assert(response.keepAliveSocket === true); - } - assert(response.status === 500); - assert(data.toString().includes('Cannot read property 'error' of undefined')); - // wait for the request data is consumed by onerror - await sleep(200); - } - }); -}); diff --git a/test/multipart.test.ts b/test/multipart.test.ts new file mode 100644 index 0000000..76b8a78 --- /dev/null +++ b/test/multipart.test.ts @@ -0,0 +1,55 @@ +import assert from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { Readable } from 'node:stream'; +import urllib from 'urllib'; +import formstream from 'formstream'; +import { mm } from 'mm'; +import { app } from './form_app.js'; +import { getFixtures } from './helper.js'; + +describe('test/multipart.test.ts', () => { + let host: string; + before(done => { + const server = app.listen(0, () => { + const addr = server.address(); + if (addr && typeof addr !== 'string') { + host = `http://127.0.0.1:${addr.port}`; + } + done(); + }); + }); + + beforeEach(() => { + mm(process.env, 'NODE_ENV', ''); + }); + + afterEach(() => { + mm.restore(); + }); + + it('should consume all request data after error throw', async () => { + // retry 10 times + for (let i = 0; i < 10; i++) { + const form = formstream(); + form.file('file1', getFixtures('bigdata.txt')); + form.field('foo', 'fengmk2') + .field('love', 'koa') + .field('index', `${i}`); + + const headers = form.headers(); + const result = await urllib.request(`${host}/upload`, { + method: 'POST', + headers, + stream: form as unknown as Readable, + timing: true, + }); + + const data = result.data; + const response = result.res; + assert.equal(response.status, 500); + assert.match(data.toString(), /TypeError: Cannot read properties of undefined/); + // wait for the request data is consumed by onerror + await scheduler.wait(200); + } + }); +}); diff --git a/test/redirect.test.js b/test/redirect.test.ts similarity index 51% rename from test/redirect.test.js rename to test/redirect.test.ts index 17aea6e..7fb7e25 100644 --- a/test/redirect.test.js +++ b/test/redirect.test.ts @@ -1,59 +1,67 @@ -'use strict'; +import koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { onerror } from '../src/index.js'; +import { mm } from 'mm'; -const koa = require('koa'); -const request = require('supertest'); -const onerror = require('..'); +describe('test/redirect.test.ts', () => { + beforeEach(() => { + mm(process.env, 'NODE_ENV', 'development'); + }); + + afterEach(() => { + mm.restore(); + }); -describe('redirect.test.js', function() { - it('should handle error and redirect to real error page', function(done) { + it('should handle error and redirect to real error page', async () => { const app = new koa(); - app.on('error', function() {}); + app.on('error', () => {}); onerror(app, { redirect: 'http://example/500.html', }); app.use(commonError); - request(app.callback()) + await request(app.callback()) .get('/') .set('Accept', 'text/html') .expect('Content-Type', 'text/html; charset=utf-8') .expect('Redirecting to http://example/500.html.') - .expect('Location', 'http://example/500.html', done); + .expect('Location', 'http://example/500.html'); }); - it('should got text/plain header', function(done) { + it('should got text/plain header', async () => { const app = new koa(); - app.on('error', function() {}); + app.on('error', () => {}); onerror(app, { redirect: 'http://example/500.html', }); app.use(commonError); - request(app.callback()) + await request(app.callback()) .get('/') .set('Accept', 'text/plain') .expect('Content-Type', 'text/plain; charset=utf-8') .expect('Redirecting to http://example/500.html.') - .expect('Location', 'http://example/500.html', done); + .expect('Location', 'http://example/500.html'); }); - it('should show json when accept is json', function(done) { + it('should show json when accept is json', async () => { const app = new koa(); - app.on('error', function() {}); + app.on('error', () => {}); onerror(app, { redirect: 'http://example/500.html', }); app.use(commonError); - request(app.callback()) + await request(app.callback()) .get('/') .set('Accept', 'application/json') .expect('Content-Type', 'application/json; charset=utf-8') - .expect({ error: 'foo is not defined' }, done); + .expect({ error: 'foo is not defined' }); }); }); function commonError() { // eslint-disable-next-line + // @ts-ignore - intentionally calling undefined function to trigger error foo(); } diff --git a/test/text.test.js b/test/text.test.js deleted file mode 100644 index b0f2a1e..0000000 --- a/test/text.test.js +++ /dev/null @@ -1,120 +0,0 @@ -'use strict'; - -const fs = require('fs'); -const koa = require('koa'); -const request = require('supertest'); -const onerror = require('..'); - -describe('text.test.js', function() { - it('should common error ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(500) - .expect('foo is not defined', done); - }); - - it('should show error message ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(exposeError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(500) - .expect('this message will be expose', done); - }); - - it('should show status error when err.message not present', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(emptyError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(500) - .expect('Internal Server Error', done); - }); - - it('should set headers from error.headers ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(headerError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(500) - .expect('foo', 'bar', done); - }); - - it('should stream error ok', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app); - app.use(streamError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(404) - .expect(/ENOENT/, done); - }); - - it('should custom handler', function(done) { - const app = new koa(); - app.on('error', function() {}); - onerror(app, { - text() { - this.status = 500; - this.body = 'error'; - }, - }); - app.use(commonError); - - request(app.callback()) - .get('/') - .set('Accept', 'text/plain') - .expect(500) - .expect('error', done); - }); -}); - -function exposeError() { - const err = new Error('this message will be expose'); - err.expose = true; - throw err; -} - -function emptyError() { - const err = new Error(''); - err.expose = true; - throw err; -} - -function commonError() { - // eslint-disable-next-line - foo(); -} - -function headerError() { - const err = new Error('error with headers'); - err.headers = { - foo: 'bar', - }; - throw err; -} - -function streamError(ctx) { - ctx.body = fs.createReadStream('not exist'); -} diff --git a/test/text.test.ts b/test/text.test.ts new file mode 100644 index 0000000..bfe94f1 --- /dev/null +++ b/test/text.test.ts @@ -0,0 +1,129 @@ +import fs from 'node:fs'; +import koa from 'koa'; +import { request } from '@eggjs/supertest'; +import { mm } from 'mm'; +import { onerror } from '../src/index.js'; +import { OnerrorError } from '../src/index.js'; + +describe('test/text.test.ts', () => { + beforeEach(() => { + mm(process.env, 'NODE_ENV', 'development'); + }); + + afterEach(() => { + mm.restore(); + }); + + it('should common error ok', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(500) + .expect('foo is not defined'); + }); + + it('should show error message ok', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app); + app.use(exposeError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(500) + .expect('this message will be expose'); + }); + + it('should show status error when err.message not present', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app); + app.use(emptyError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(500) + .expect('Internal Server Error'); + }); + + it('should set headers from error.headers ok', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app); + app.use(headerError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(500) + .expect('foo', 'bar'); + }); + + it('should stream error ok', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app); + app.use(streamError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(404) + .expect(/ENOENT/); + }); + + it('should custom handler', async () => { + const app = new koa(); + app.on('error', () => {}); + onerror(app, { + text(this: any) { + this.status = 500; + this.body = 'error'; + }, + }); + app.use(commonError); + + await request(app.callback()) + .get('/') + .set('Accept', 'text/plain') + .expect(500) + .expect('error'); + }); +}); + +function exposeError() { + const err = new Error('this message will be expose') as OnerrorError; + err.expose = true; + throw err; +} + +function emptyError() { + const err = new Error('') as OnerrorError; + err.expose = true; + throw err; +} + +function commonError() { + // eslint-disable-next-line + // @ts-ignore - intentionally calling undefined function to trigger error + foo(); +} + +function headerError() { + const err = new Error('error with headers') as OnerrorError; + err.headers = { + foo: 'bar', + }; + throw err; +} + +function streamError(ctx: any) { + ctx.body = fs.createReadStream('not exist'); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}