diff --git a/.eslintrc b/.eslintrc index c799fe5327..9bcdb46887 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/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 444235616c..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 1 -updates: - - package-ecosystem: npm - directory: "/" - schedule: - interval: weekly - open-pull-requests-limit: 5 diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 559003fa3a..de8e7612c4 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@master - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} diff --git a/README.md b/README.md index 602a358413..d27b41976e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ English | [简体中文](./README.zh-CN.md) [![Known Vulnerabilities](https://snyk.io/test/npm/egg/badge.svg?style=flat-square)](https://snyk.io/test/npm/egg) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/eggjs?style=flat-square)](https://opencollective.com/eggjs) - ## Features - Built-in Process Management @@ -62,5 +61,4 @@ To become a contributor, please follow our [contributing guide](CONTRIBUTING.md) [MIT](LICENSE) - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fegg.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fegg?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fegg.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fegg?ref=badge_large) diff --git a/README.zh-CN.md b/README.zh-CN.md index 0967d1c94b..493aafdb0e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -25,11 +25,12 @@ ## 快速开始 ```bash -$ mkdir showcase && cd showcase -$ npm init egg --type=simple -$ npm install -$ npm run dev -$ open http://localhost:7001 +mkdir showcase && cd showcase +npm init egg --type=simple +npm install +npm run dev + +open http://localhost:7001 ``` ## 文档 diff --git a/app/middleware/body_parser.js b/app/middleware/body_parser.js deleted file mode 100644 index 70f473a33d..0000000000 --- a/app/middleware/body_parser.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('koa-bodyparser'); diff --git a/app/middleware/override_method.js b/app/middleware/override_method.js deleted file mode 100644 index bf5f7ef0f4..0000000000 --- a/app/middleware/override_method.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports = require('koa-override'); diff --git a/app/middleware/site_file.js b/app/middleware/site_file.js deleted file mode 100644 index 4e3ff16b2c..0000000000 --- a/app/middleware/site_file.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const path = require('path'); - -module.exports = options => { - return async function siteFile(ctx, next) { - if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return next(); - /* istanbul ignore if */ - if (ctx.path[0] !== '/') return next(); - - let content = options[ctx.path]; - if (!content) return next(); - - // '/favicon.ico': 'https://eggjs.org/favicon.ico' or '/favicon.ico': async (ctx) => 'https://eggjs.org/favicon.ico' - // content is function - if (typeof content === 'function') content = await content(ctx); - // content is url - if (typeof content === 'string') return ctx.redirect(content); - - // '/robots.txt': Buffer = this.dnsCacheLookupInterval) { - // make sure the next request doesn't refresh dns query - record.timestamp = now; - this[UPDATE_DNS](hostname, args).catch(err => this.app.emit('error', err)); - } - - return { url: formatDnsLookupUrl(hostname, url, record.ip), args }; - } - - const address = await this[UPDATE_DNS](hostname, args); - return { url: formatDnsLookupUrl(hostname, url, address), args }; - } - - async [UPDATE_DNS](hostname, args) { - const logger = args.ctx ? args.ctx.coreLogger : this.app.coreLogger; - try { - const { address } = await dns.lookup(hostname, { family: 4 }); - logger.info('[dnscache_httpclient] dns lookup success: %s => %s', - hostname, address); - this.dnsCache.set(hostname, { timestamp: Date.now(), ip: address }); - return address; - } catch (err) { - err.message = `[dnscache_httpclient] dns lookup error: ${hostname} => ${err.message}`; - throw err; - } - } -} - -module.exports = DNSCacheHttpClient; - -function formatDnsLookupUrl(host, url, address) { - if (typeof url === 'string') return url.replace(host, address); - const urlObj = assign({}, url); - urlObj.hostname = urlObj.hostname.replace(host, address); - if (urlObj.host) { - urlObj.host = urlObj.host.replace(host, address); - } - return urlObj; -} diff --git a/lib/core/httpclient.js b/lib/core/httpclient.js deleted file mode 100644 index a270a8c026..0000000000 --- a/lib/core/httpclient.js +++ /dev/null @@ -1,108 +0,0 @@ -const Agent = require('agentkeepalive'); -const HttpsAgent = require('agentkeepalive').HttpsAgent; -const urllib = require('urllib'); -const ms = require('humanize-ms'); -const { FrameworkBaseError } = require('egg-errors'); - -class HttpClientError extends FrameworkBaseError { - get module() { - return 'httpclient'; - } -} - -class HttpClient extends urllib.HttpClient2 { - constructor(app) { - normalizeConfig(app); - const config = app.config.httpclient; - super({ - app, - defaultArgs: config.request, - agent: new Agent(config.httpAgent), - httpsAgent: new HttpsAgent(config.httpsAgent), - }); - this.app = app; - } - - async request(url, args) { - args = args || {}; - if (args.ctx && args.ctx.tracer) { - args.tracer = args.ctx.tracer; - } else { - args.tracer = args.tracer || this.app.tracer; - } - - try { - return await super.request(url, args); - } catch (err) { - if (err.code === 'ENETUNREACH') { - throw HttpClientError.create(err.message, err.code); - } - throw err; - } - } - - async curl(...args) { - return await this.request(...args); - } -} - -function normalizeConfig(app) { - const config = app.config.httpclient; - - // compatibility - if (typeof config.keepAlive === 'boolean') { - config.httpAgent.keepAlive = config.keepAlive; - config.httpsAgent.keepAlive = config.keepAlive; - } - if (config.timeout) { - config.timeout = ms(config.timeout); - config.httpAgent.timeout = config.timeout; - config.httpsAgent.timeout = config.timeout; - } - // compatibility httpclient.freeSocketKeepAliveTimeout => httpclient.freeSocketTimeout - if (config.freeSocketKeepAliveTimeout && !config.freeSocketTimeout) { - config.freeSocketTimeout = config.freeSocketKeepAliveTimeout; - delete config.freeSocketKeepAliveTimeout; - } - if (config.freeSocketTimeout) { - config.freeSocketTimeout = ms(config.freeSocketTimeout); - config.httpAgent.freeSocketTimeout = config.freeSocketTimeout; - config.httpsAgent.freeSocketTimeout = config.freeSocketTimeout; - } else { - // compatibility agent.freeSocketKeepAliveTimeout - if (config.httpAgent.freeSocketKeepAliveTimeout && !config.httpAgent.freeSocketTimeout) { - config.httpAgent.freeSocketTimeout = config.httpAgent.freeSocketKeepAliveTimeout; - delete config.httpAgent.freeSocketKeepAliveTimeout; - } - if (config.httpsAgent.freeSocketKeepAliveTimeout && !config.httpsAgent.freeSocketTimeout) { - config.httpsAgent.freeSocketTimeout = config.httpsAgent.freeSocketKeepAliveTimeout; - delete config.httpsAgent.freeSocketKeepAliveTimeout; - } - } - - if (typeof config.maxSockets === 'number') { - config.httpAgent.maxSockets = config.maxSockets; - config.httpsAgent.maxSockets = config.maxSockets; - } - if (typeof config.maxFreeSockets === 'number') { - config.httpAgent.maxFreeSockets = config.maxFreeSockets; - config.httpsAgent.maxFreeSockets = config.maxFreeSockets; - } - - if (config.httpAgent.timeout < 30000) { - app.coreLogger.warn('[egg:httpclient] config.httpclient.httpAgent.timeout(%s) can\'t below 30000, auto reset to 30000', - config.httpAgent.timeout); - config.httpAgent.timeout = 30000; - } - if (config.httpsAgent.timeout < 30000) { - app.coreLogger.warn('[egg:httpclient] config.httpclient.httpsAgent.timeout(%s) can\'t below 30000, auto reset to 30000', - config.httpsAgent.timeout); - config.httpsAgent.timeout = 30000; - } - - if (typeof config.request.timeout === 'string') { - config.request.timeout = ms(config.request.timeout); - } -} - -module.exports = HttpClient; diff --git a/lib/core/httpclient_next.js b/lib/core/httpclient_next.js deleted file mode 100644 index 266338d522..0000000000 --- a/lib/core/httpclient_next.js +++ /dev/null @@ -1,37 +0,0 @@ -const { HttpClient } = require('urllib-next'); -const ms = require('humanize-ms'); - -class HttpClientNext extends HttpClient { - constructor(app) { - normalizeConfig(app); - const config = app.config.httpclient; - super({ - app, - defaultArgs: config.request, - }); - this.app = app; - } - - async request(url, options) { - options = options || {}; - if (options.ctx && options.ctx.tracer) { - options.tracer = options.ctx.tracer; - } else { - options.tracer = options.tracer || this.app.tracer; - } - return await super.request(url, options); - } - - async curl(...args) { - return await this.request(...args); - } -} - -function normalizeConfig(app) { - const config = app.config.httpclient; - if (typeof config.request.timeout === 'string') { - config.request.timeout = ms(config.request.timeout); - } -} - -module.exports = HttpClientNext; diff --git a/package.json b/package.json index 6bb6658052..bedc098374 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "publishConfig": { "tag": "next" }, - "description": "A web framework's framework for Node.js", + "description": "A web application framework for Node.js", "keywords": [ "web", "app", @@ -19,18 +19,15 @@ "egg" ], "dependencies": { + "@eggjs/cookies": "^3.0.0", + "@eggjs/core": "^6.0.1", "@types/accepts": "^1.3.5", - "@types/koa": "^2.13.5", - "@types/koa-router": "^7.4.4", "accepts": "^1.3.8", - "agentkeepalive": "^4.2.1", - "cache-content-type": "^1.0.1", + "cache-content-type": "^2.0.0", "circular-json-for-egg": "^1.0.0", - "cluster-client": "^3.3.0", + "cluster-client": "^3.7.0", "delegates": "^1.0.0", - "egg-cluster": "^2.0.0", - "egg-cookies": "^2.6.1", - "egg-core": "^5.4.0", + "egg-cluster": "^2.3.0", "egg-development": "^3.0.0", "egg-errors": "^2.3.1", "egg-i18n": "^2.1.1", @@ -45,42 +42,43 @@ "egg-static": "^2.2.0", "egg-view": "^2.1.3", "egg-watcher": "^3.1.1", - "extend2": "^1.0.1", + "extend2": "^4.0.0", "graceful": "^1.1.0", - "humanize-ms": "^1.2.1", "is-type-of": "^2.1.0", "koa-bodyparser": "^4.4.1", "koa-is-json": "^1.0.0", - "koa-override": "^3.0.0", + "koa-override": "^4.0.0", "ms": "^2.1.3", "on-finished": "^2.4.1", "onelogger": "^1.0.0", "sendmessage": "^2.0.0", - "urllib": "^2.33.0", - "urllib-next": "npm:urllib@^3.22.4", + "urllib": "^4.0.0", "utility": "^2.1.0", "ylru": "^1.3.2" }, "devDependencies": { - "@eggjs/tsconfig": "^1.1.0", - "@types/node": "^20.1.2", + "@arethetypeswrong/cli": "^0.15.3", + "@eggjs/tsconfig": "1", + "@types/koa-bodyparser": "^4.3.12", + "@types/ms": "^0.7.34", + "@types/node": "20", "@umijs/preset-react": "^2.1.6", - "address": "^1.2.1", - "antd": "^4.23.2", - "assert-file": "^1.0.0", - "coffee": "^5.4.0", - "cross-env": "^7.0.3", + "address": "2", + "antd": "4", + "assert-file": "1", + "coffee": "5", + "cross-env": "7", "dumi": "^1.1.47", "dumi-theme-egg": "^1.2.2", - "egg-bin": "^6.4.1", - "egg-mock": "^5.10.7", + "egg-bin": "6", + "egg-mock": "^5.12.0", "egg-plugin-puml": "^2.4.0", - "egg-tracer": "^2.0.0", + "egg-tracer": "^2.1.0", "egg-view-nunjucks": "^2.3.0", - "eslint": "^8.23.1", - "eslint-config-egg": "^12.0.0", + "eslint": "8", + "eslint-config-egg": "13", "findlinks": "^2.2.0", - "formstream": "^1.1.1", + "formstream": "^1.5.1", "jsdoc": "^3.6.11", "koa": "^2.13.4", "koa-static": "^5.0.0", @@ -95,41 +93,63 @@ "sdk-base": "^4.2.1", "spy": "^1.0.0", "supertest": "^6.2.4", - "ts-node": "^10.9.1", - "tsd": "^0.28.1", - "typescript": "^5.0.4", + "ts-node": "10", + "tsd": "^0.31.1", + "typescript": "5", "umi": "^3.5.36" }, - "main": "index.js", - "types": "index.d.ts", - "files": [ - "index.js", - "lib", - "app", - "config", - "agent.js", - "index.d.ts" - ], "scripts": { "lint": "eslint app config lib test *.js", "tsd": "tsd", - "test": "npm run lint -- --fix && npm run tsd && npm run test-local", - "test-local": "egg-bin test --ts false", - "test-local-changed": "egg-bin test --changed --ts false", - "cov": "egg-bin cov --timeout 100000 --ts false", - "ci": "npm run lint && npm run tsd && npm run cov", + "pretest": "npm run lint -- --fix && npm run tsd", + "test": "egg-bin test", + "test:changed": "egg-bin test --changed", + "cov": "egg-bin cov --timeout 100000", + "preci": "npm run lint && npm run tsd", + "ci": "npm run cov && npm run prepublishOnly && attw --pack", + "prepublishOnly": "tshy && tshy-after", "site:dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider APP_ROOT=./site dumi dev", "site:devWithNode14-16": "cross-env APP_ROOT=./site dumi dev", "site:build": "cross-env NODE_OPTIONS=--openssl-legacy-provider APP_ROOT=./site dumi build", "site:buildWithNode14-16": "cross-env APP_ROOT=./site dumi build", "site:prettier": "prettier --config site/.prettierrc --ignore-path site/.prettierignore --write \"site/**/*.{js,jsx,tsx,ts,less,md,json}\"", - "puml": "puml . --dest ./site", - "commits": "./scripts/commits.sh" + "puml": "puml . --dest ./site" }, "homepage": "https://github.com/eggjs/egg", "repository": { "type": "git", - "url": "https://github.com/eggjs/egg.git" + "url": "git://github.com/eggjs/egg.git" + }, + "license": "MIT", + "egg": { + "framework": true, + "baseDir": { + "import": "./dist/esm", + "require": "./dist/commonjs" + } + }, + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "source": "./src/index.ts", + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "source": "./src/index.ts", + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } }, - "license": "MIT" + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js" } diff --git a/scripts/commits.sh b/scripts/commits.sh deleted file mode 100755 index 3672a615a5..0000000000 --- a/scripts/commits.sh +++ /dev/null @@ -1,9 +0,0 @@ -#! /usr/bin/env bash - -REPO=$(git config --get remote.origin.url | sed 's/git@\(.*\):\(.*\)\.git/http:\/\/\1\/\2/g') -LAST_TAG=$(git describe --tags --abbrev=0) -LAST_TAG_DATE=$(git show -s --format=%cd $LAST_TAG) -FORMAT=" * [[\`%h\`]($REPO/commit/%H)] - %s (%aN <<%ae>>)" - -git fetch -git log --pretty=format:"$FORMAT" --since="$LAST_TAG_DATE" --no-merges diff --git a/agent.js b/src/agent.js similarity index 100% rename from agent.js rename to src/agent.js diff --git a/app/extend/context.js b/src/app/extend/context.js similarity index 100% rename from app/extend/context.js rename to src/app/extend/context.js diff --git a/app/extend/helper.js b/src/app/extend/helper.js similarity index 100% rename from app/extend/helper.js rename to src/app/extend/helper.js diff --git a/app/extend/request.js b/src/app/extend/request.js similarity index 100% rename from app/extend/request.js rename to src/app/extend/request.js diff --git a/app/extend/response.js b/src/app/extend/response.js similarity index 100% rename from app/extend/response.js rename to src/app/extend/response.js diff --git a/src/app/middleware/body_parser.ts b/src/app/middleware/body_parser.ts new file mode 100644 index 0000000000..f9fd3015c5 --- /dev/null +++ b/src/app/middleware/body_parser.ts @@ -0,0 +1,3 @@ +import bodyparser from 'koa-bodyparser'; + +export default bodyparser; diff --git a/app/middleware/meta.js b/src/app/middleware/meta.ts similarity index 51% rename from app/middleware/meta.js rename to src/app/middleware/meta.ts index 9e5283f53c..dc9e738469 100644 --- a/app/middleware/meta.js +++ b/src/app/middleware/meta.ts @@ -2,12 +2,20 @@ * meta middleware, should be the first middleware */ -const { performance } = require('perf_hooks'); +import { performance } from 'node:perf_hooks'; +import type { EggCoreContext } from '@eggjs/core'; +import type { Next } from '../../lib/type.js'; -module.exports = options => { - return async function meta(ctx, next) { +export interface MetaMiddlewareOptions { + enable: boolean; + logging: boolean; +} + +export default (options: MetaMiddlewareOptions) => { + return async function meta(ctx: EggCoreContext, next: Next) { if (options.logging) { - ctx.coreLogger.info('[meta] request started, host: %s, user-agent: %s', ctx.host, ctx.header['user-agent']); + ctx.coreLogger.info('[meta] request started, host: %s, user-agent: %s', + ctx.host, ctx.header['user-agent']); } await next(); // total response time header diff --git a/app/middleware/notfound.js b/src/app/middleware/notfound.ts similarity index 69% rename from app/middleware/notfound.js rename to src/app/middleware/notfound.ts index 4ec4288f8e..0f189fc3d5 100644 --- a/app/middleware/notfound.js +++ b/src/app/middleware/notfound.ts @@ -1,7 +1,13 @@ -'use strict'; +import type { EggCoreContext } from '@eggjs/core'; +import type { Next } from '../../lib/type.js'; -module.exports = options => { - return async function notfound(ctx, next) { +export interface NotFoundMiddlewareOptions { + enable: boolean; + pageUrl: string; +} + +export default (options: NotFoundMiddlewareOptions) => { + return async function notfound(ctx: EggCoreContext, next: Next) { await next(); if (ctx.status !== 404 || ctx.body) { diff --git a/src/app/middleware/override_method.ts b/src/app/middleware/override_method.ts new file mode 100644 index 0000000000..e5b33b0458 --- /dev/null +++ b/src/app/middleware/override_method.ts @@ -0,0 +1,3 @@ +import override from 'koa-override'; + +export default override; diff --git a/src/app/middleware/site_file.ts b/src/app/middleware/site_file.ts new file mode 100644 index 0000000000..1ba848a3c9 --- /dev/null +++ b/src/app/middleware/site_file.ts @@ -0,0 +1,49 @@ +import path from 'node:path'; +import type { EggCoreContext } from '@eggjs/core'; +import type { Next } from '../../lib/type.js'; + +export type SiteFileContentFun = (ctx: EggCoreContext) => Promise; + +export interface SiteFileMiddlewareOptions { + enable: boolean; + cacheControl: string; + [key: string]: string | Buffer | boolean | SiteFileContentFun; +} + +module.exports = (options: SiteFileMiddlewareOptions) => { + return async function siteFile(ctx: EggCoreContext, next: Next) { + if (ctx.method !== 'HEAD' && ctx.method !== 'GET') { + return next(); + } + /* istanbul ignore if */ + if (ctx.path[0] !== '/') { + return next(); + } + + let content = options[ctx.path]; + if (!content) { + return next(); + } + + // '/favicon.ico': 'https://eggjs.org/favicon.ico' or '/favicon.ico': async (ctx) => 'https://eggjs.org/favicon.ico' + // content is function + if (typeof content === 'function') { + content = await content(ctx); + } + // content is url + if (typeof content === 'string') { + return ctx.redirect(content); + } + + // '/robots.txt': Buffer { - - const config = { - +export default (appInfo: EggAppInfo) => { + const config: Partial = { /** * The environment of egg * @member {String} Config#env @@ -131,7 +128,7 @@ module.exports = appInfo => { HOME: appInfo.HOME, /** - * The directory of server running. You can find `application_config.json` under it that is dumpped from `app.config`. + * The directory of server running. You can find `application_config.json` under it that is dumped from `app.config`. * @member {String} Config#rundir * @default * @since 1.0.0 @@ -153,7 +150,7 @@ module.exports = appInfo => { /secret/i, ]), timing: { - // if boot action >= slowBootActionMinDuration, egg core will print it to warnning log + // if boot action >= slowBootActionMinDuration, egg core will print it to warning log slowBootActionMinDuration: 5000, }, }, @@ -176,7 +173,7 @@ module.exports = appInfo => { }; /** - * The option of `notfound` middleware + * The options of `notfound` middleware * * It will return page or json depend on negotiation when 404, * If pageUrl is set, it will redirect to the page. @@ -185,6 +182,7 @@ module.exports = appInfo => { * @property {String} pageUrl - the 404 page url */ config.notfound = { + enable: true, pageUrl: '', }; @@ -202,13 +200,14 @@ module.exports = appInfo => { * }; */ config.siteFile = { + enable: true, '/favicon.ico': fs.readFileSync(path.join(__dirname, 'favicon.png')), // default cache in 30 days cacheControl: 'public, max-age=2592000', }; /** - * The option of `bodyParser` middleware + * The options of `bodyParser` middleware * * @member Config#bodyParser * @property {Boolean} enable - enable bodyParser or not, default is true @@ -236,8 +235,9 @@ module.exports = appInfo => { depth: 5, parameterLimit: 1000, }, + onProtoPoisoning: 'error', onerror(err, ctx) { - err.message += ', check bodyParser config'; + err.message = `${err.message}, check bodyParser config`; if (ctx.status === 404) { // set default status to 400, meaning client bad request ctx.status = 400; @@ -264,8 +264,8 @@ module.exports = appInfo => { * @property {String} agentLogName - file name of agent worker log * @property {Object} coreLogger - custom config of coreLogger * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false - * @property {Boolean} enablePerformanceTimer - using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to false. e.g.: logger will set 1.456ms instead of 1ms. - * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false + * @property {Boolean} enablePerformanceTimer - using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to true. e.g.: logger will set 1.456ms instead of 1ms. + * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to true */ config.logger = { dir: path.join(appInfo.root, 'logs', appInfo.name), @@ -282,8 +282,8 @@ module.exports = appInfo => { errorLogName: 'common-error.log', coreLogger: {}, allowDebugAtProd: false, - enablePerformanceTimer: false, - enableFastContextLogger: false, + enablePerformanceTimer: true, + enableFastContextLogger: true, }; /** @@ -306,34 +306,17 @@ module.exports = appInfo => { * @property {Boolean} useHttpClientNext - use urllib@3 HttpClient */ config.httpclient = { - enableDNSCache: false, - dnsCacheLookupInterval: 10000, - dnsCacheMaxLength: 1000, - request: { timeout: 5000, }, - httpAgent: { - keepAlive: true, - freeSocketTimeout: 4000, - maxSockets: Number.MAX_SAFE_INTEGER, - maxFreeSockets: 256, - }, - httpsAgent: { - keepAlive: true, - freeSocketTimeout: 4000, - maxSockets: Number.MAX_SAFE_INTEGER, - maxFreeSockets: 256, - }, - useHttpClientNext: false, }; /** - * The option of `meta` middleware + * The options of `meta` middleware * * @member Config#meta - * @property {Boolean} enable - enable meta or not, default is true - * @property {Boolean} logging - enable logging start request, default is false + * @property {Boolean} enable - enable meta or not, default is `true` + * @property {Boolean} logging - enable logging start request, default is `false` */ config.meta = { enable: true, @@ -369,7 +352,7 @@ module.exports = appInfo => { config.serverTimeout = null; /** - * + * The options of cluster * @member {Object} Config#cluster * @property {Object} listen - listen options, see {@link https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback} * @property {String} listen.path - set a unix sock path when server listen @@ -416,7 +399,7 @@ module.exports = appInfo => { * }; * } */ - config.onClientError = null; + config.onClientError = undefined; return config; }; diff --git a/src/config/config.local.ts b/src/config/config.local.ts new file mode 100644 index 0000000000..af29a4c62f --- /dev/null +++ b/src/config/config.local.ts @@ -0,0 +1,11 @@ +import type { EggAppConfig } from '../lib/type.js'; + +export default () => { + return { + logger: { + coreLogger: { + consoleLevel: 'WARN', + }, + }, + } satisfies Partial; +} diff --git a/src/config/config.unittest.ts b/src/config/config.unittest.ts new file mode 100644 index 0000000000..95eacc6ab5 --- /dev/null +++ b/src/config/config.unittest.ts @@ -0,0 +1,10 @@ +import type { EggAppConfig } from '../lib/type.js'; + +export default () => { + return { + logger: { + consoleLevel: 'WARN', + buffer: false, + }, + } satisfies Partial; +} diff --git a/config/favicon.png b/src/config/favicon.png similarity index 100% rename from config/favicon.png rename to src/config/favicon.png diff --git a/config/plugin.js b/src/config/plugin.ts similarity index 98% rename from config/plugin.js rename to src/config/plugin.ts index da864347eb..9c0a8491ed 100644 --- a/config/plugin.js +++ b/src/config/plugin.ts @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export default { // enable plugins /** diff --git a/index.d.ts b/src/index.d.ts similarity index 100% rename from index.d.ts rename to src/index.d.ts diff --git a/index.js b/src/index.js similarity index 100% rename from index.js rename to src/index.js diff --git a/index.test-d.ts b/src/index.test-d.ts similarity index 100% rename from index.test-d.ts rename to src/index.test-d.ts diff --git a/lib/agent.js b/src/lib/agent.js similarity index 100% rename from lib/agent.js rename to src/lib/agent.js diff --git a/lib/application.js b/src/lib/application.js similarity index 100% rename from lib/application.js rename to src/lib/application.js diff --git a/lib/core/base_context_class.js b/src/lib/core/base_context_class.ts similarity index 59% rename from lib/core/base_context_class.js rename to src/lib/core/base_context_class.ts index 1016d60440..882b9a514d 100644 --- a/lib/core/base_context_class.js +++ b/src/lib/core/base_context_class.ts @@ -1,7 +1,5 @@ -'use strict'; - -const EggCoreBaseContextClass = require('egg-core').BaseContextClass; -const BaseContextLogger = require('./base_context_logger'); +import { BaseContextClass as EggCoreBaseContextClass } from '@eggjs/core'; +import { BaseContextLogger } from './base_context_logger'; const LOGGER = Symbol('BaseContextClass#logger'); @@ -10,11 +8,11 @@ const LOGGER = Symbol('BaseContextClass#logger'); * it's instantiated in context level, * {@link Helper}, {@link Service} is extending it. */ -class BaseContextClass extends EggCoreBaseContextClass { +export class BaseContextClass extends EggCoreBaseContextClass { + protected pathName?: string; + get logger() { if (!this[LOGGER]) this[LOGGER] = new BaseContextLogger(this.ctx, this.pathName); return this[LOGGER]; } } - -module.exports = BaseContextClass; diff --git a/lib/core/base_context_logger.js b/src/lib/core/base_context_logger.ts similarity index 62% rename from lib/core/base_context_logger.js rename to src/lib/core/base_context_logger.ts index 6c1dc2c8df..b73758c722 100644 --- a/lib/core/base_context_logger.js +++ b/src/lib/core/base_context_logger.ts @@ -1,27 +1,32 @@ +import { type EggCoreContext } from '@eggjs/core'; + const CALL = Symbol('BaseContextLogger#call'); -class BaseContextLogger { +export class BaseContextLogger { + readonly #ctx: EggCoreContext; + readonly #pathName?: string; + /** * @class * @param {Context} ctx - context instance * @param {String} pathName - class path name * @since 1.0.0 */ - constructor(ctx, pathName) { + constructor(ctx: EggCoreContext, pathName?: string) { /** * @member {Context} BaseContextLogger#ctx * @since 1.2.0 */ - this.ctx = ctx; - this.pathName = pathName; + this.#ctx = ctx; + this.#pathName = pathName; } - [CALL](method, args) { + [CALL](method: string, args: any[]) { // add `[${pathName}]` in log - if (this.pathName && typeof args[0] === 'string') { - args[0] = `[${this.pathName}] ${args[0]}`; + if (this.#pathName && typeof args[0] === 'string') { + args[0] = `[${this.#pathName}] ${args[0]}`; } - this.ctx.app.logger[method](...args); + this.#ctx.app.logger[method](...args); } /** @@ -29,7 +34,7 @@ class BaseContextLogger { * @param {...any} args - log msg * @since 1.2.0 */ - debug(...args) { + debug(...args: any[]) { this[CALL]('debug', args); } @@ -38,7 +43,7 @@ class BaseContextLogger { * @param {...any} args - log msg * @since 1.2.0 */ - info(...args) { + info(...args: any[]) { this[CALL]('info', args); } @@ -47,7 +52,7 @@ class BaseContextLogger { * @param {...any} args - log msg * @since 1.2.0 */ - warn(...args) { + warn(...args: any[]) { this[CALL]('warn', args); } @@ -56,9 +61,7 @@ class BaseContextLogger { * @param {...any} args - log msg * @since 1.2.0 */ - error(...args) { + error(...args: any[]) { this[CALL]('error', args); } } - -module.exports = BaseContextLogger; diff --git a/lib/core/base_hook_class.js b/src/lib/core/base_hook_class.ts similarity index 83% rename from lib/core/base_hook_class.js rename to src/lib/core/base_hook_class.ts index 2bda13bf60..a310e2fbe4 100644 --- a/lib/core/base_hook_class.js +++ b/src/lib/core/base_hook_class.ts @@ -1,9 +1,9 @@ -'use strict'; +import assert from 'node:assert'; -const assert = require('assert'); const INSTANCE = Symbol('BaseHookClass#instance'); -class BaseHookClass { +export class BaseHookClass { + constructor(instance) { this[INSTANCE] = instance; @@ -27,5 +27,3 @@ class BaseHookClass { return this[INSTANCE]; } } - -module.exports = BaseHookClass; diff --git a/lib/core/context_httpclient.js b/src/lib/core/context_httpclient.js similarity index 100% rename from lib/core/context_httpclient.js rename to src/lib/core/context_httpclient.js diff --git a/src/lib/core/httpclient.ts b/src/lib/core/httpclient.ts new file mode 100644 index 0000000000..d2f084f549 --- /dev/null +++ b/src/lib/core/httpclient.ts @@ -0,0 +1,49 @@ +import { EggCoreContext } from '@eggjs/core'; +import { + HttpClient as RawHttpClient, + RequestURL, RequestOptions, +} from 'urllib'; +import ms from 'ms'; +import type { EggApplication } from '../egg.js'; + +export type { HttpClientResponse } from 'urllib'; +export type HttpClientRequestURL = RequestURL; + +export interface HttpClientRequestOptions extends RequestOptions { + ctx?: EggCoreContext; + tracer?: unknown; +} + +export class HttpClient extends RawHttpClient { + readonly #app: EggApplication & { tracer?: unknown }; + + constructor(app: EggApplication) { + normalizeConfig(app); + const config = app.config.httpclient; + super({ + defaultArgs: config.request, + }); + this.#app = app; + } + + async request(url: RequestURL, options?: HttpClientRequestOptions) { + options = options ?? {}; + if (options.ctx?.tracer) { + options.tracer = options.ctx.tracer; + } else { + options.tracer = options.tracer ?? this.#app.tracer; + } + return await super.request(url, options); + } + + async curl(url: RequestURL, options?: HttpClientRequestOptions) { + return await this.request(url, options); + } +} + +function normalizeConfig(app: EggApplication) { + const config = app.config.httpclient; + if (typeof config.request.timeout === 'string') { + config.request.timeout = ms(config.request.timeout); + } +} diff --git a/lib/core/logger.js b/src/lib/core/logger.ts similarity index 65% rename from lib/core/logger.js rename to src/lib/core/logger.ts index a38dbddcf5..745796aa95 100644 --- a/lib/core/logger.js +++ b/src/lib/core/logger.ts @@ -1,7 +1,10 @@ -const { EggLoggers } = require('egg-logger'); -const { setCustomLogger } = require('onelogger'); +import { EggLoggers } from 'egg-logger'; +import { setCustomLogger } from 'onelogger'; +import type { EggApplication } from '../egg.js'; -module.exports = function createLoggers(app) { +export type { EggLoggers, EggLogger } from 'egg-logger'; + +export function createLoggers(app: EggApplication) { const loggerConfig = app.config.logger; loggerConfig.type = app.type; loggerConfig.localStorage = app.ctxStorage; @@ -16,6 +19,7 @@ module.exports = function createLoggers(app) { app.ready(() => { if (loggerConfig.disableConsoleAfterReady) { loggers.disableConsole(); + loggers.coreLogger.info('[egg:lib:core:logger] disable console log after app ready'); } }); @@ -29,7 +33,6 @@ module.exports = function createLoggers(app) { setCustomLogger(loggerName, undefined); } }); - loggers.coreLogger.info('[egg:logger] init all loggers with options: %j', loggerConfig); - + loggers.coreLogger.info('[egg:lib:core:logger] init all loggers with options: %j', loggerConfig); return loggers; -}; +} diff --git a/lib/core/messenger/index.js b/src/lib/core/messenger/index.js similarity index 100% rename from lib/core/messenger/index.js rename to src/lib/core/messenger/index.js diff --git a/lib/core/messenger/ipc.js b/src/lib/core/messenger/ipc.js similarity index 100% rename from lib/core/messenger/ipc.js rename to src/lib/core/messenger/ipc.js diff --git a/lib/core/messenger/local.js b/src/lib/core/messenger/local.js similarity index 100% rename from lib/core/messenger/local.js rename to src/lib/core/messenger/local.js diff --git a/lib/core/singleton.js b/src/lib/core/singleton.ts similarity index 66% rename from lib/core/singleton.js rename to src/lib/core/singleton.ts index d0b9c29e27..6b50cfd142 100644 --- a/lib/core/singleton.js +++ b/src/lib/core/singleton.ts @@ -1,24 +1,36 @@ -'use strict'; +import assert from 'node:assert'; +import { isAsyncFunction } from 'is-type-of'; +import type { EggApplication } from '../egg.js'; -const assert = require('assert'); -const is = require('is-type-of'); +export type SingletonCreateMethod = + (config: Record, app: EggApplication, clientName: string) => unknown | Promise; -class Singleton { - constructor(options = {}) { +export interface SingletonOptions { + name: string; + app: EggApplication; + create: SingletonCreateMethod; +} + +export class Singleton { + readonly clients = new Map(); + readonly app: EggApplication; + readonly create: SingletonCreateMethod; + readonly name: string; + readonly options: Record; + + constructor(options: SingletonOptions) { assert(options.name, '[egg:singleton] Singleton#constructor options.name is required'); assert(options.app, '[egg:singleton] Singleton#constructor options.app is required'); assert(options.create, '[egg:singleton] Singleton#constructor options.create is required'); assert(!options.app[options.name], `${options.name} is already exists in app`); - this.clients = new Map(); this.app = options.app; this.name = options.name; this.create = options.create; - /* istanbul ignore next */ - this.options = options.app.config[this.name] || {}; + this.options = options.app.config[this.name] ?? {}; } init() { - return is.asyncFunction(this.create) ? this.initAsync() : this.initSync(); + return isAsyncFunction(this.create) ? this.initAsync() : this.initSync(); } initSync() { @@ -30,7 +42,7 @@ class Singleton { if (options.client) { const client = this.createInstance(options.client, options.name); this.app[this.name] = client; - this._extendDynamicMethods(client); + this.#extendDynamicMethods(client); return; } @@ -57,13 +69,13 @@ class Singleton { if (options.client) { const client = await this.createInstanceAsync(options.client, options.name); this.app[this.name] = client; - this._extendDynamicMethods(client); + this.#extendDynamicMethods(client); return; } // multi client, use app[name].getInstance(id) if (options.clients) { - await Promise.all(Object.keys(options.clients).map(id => { + await Promise.all(Object.keys(options.clients).map((id: string) => { return this.createInstanceAsync(options.clients[id], id) .then(client => this.clients.set(id, client)); })); @@ -75,31 +87,37 @@ class Singleton { this.app[this.name] = this; } - get(id) { + get(id: string) { return this.clients.get(id); } // alias to `get(id)` - getSingletonInstance(id) { + getSingletonInstance(id: string) { return this.clients.get(id); } - createInstance(config, clientName) { + createInstance(config: Record, clientName: string) { // async creator only support createInstanceAsync - assert(!is.asyncFunction(this.create), + assert(!isAsyncFunction(this.create), `egg:singleton ${this.name} only support create asynchronous, please use createInstanceAsync`); // options.default will be merge in to options.clients[id] - config = Object.assign({}, this.options.default, config); - return this.create(config, this.app, clientName); + config = { + ...this.options.default, + ...config, + } + return (this.create as SingletonCreateMethod)(config, this.app, clientName); } - async createInstanceAsync(config, clientName) { + async createInstanceAsync(config: Record, clientName: string) { // options.default will be merge in to options.clients[id] - config = Object.assign({}, this.options.default, config); + config = { + ...this.options.default, + ...config, + } return await this.create(config, this.app, clientName); } - _extendDynamicMethods(client) { + #extendDynamicMethods(client: any) { assert(!client.createInstance, 'singleton instance should not have createInstance method'); assert(!client.createInstanceAsync, 'singleton instance should not have createInstanceAsync method'); @@ -113,9 +131,10 @@ class Singleton { extendable.createInstance = this.createInstance.bind(this); extendable.createInstanceAsync = this.createInstanceAsync.bind(this); } catch (err) { - this.app.logger.warn('egg:singleton %s dynamic create is disabled because of client is unextensible', this.name); + this.app.logger.warn( + 'egg:singleton %s dynamic create is disabled because of client is un-extendable', + this.name); + this.app.logger.warn(err); } } } - -module.exports = Singleton; diff --git a/lib/core/utils.js b/src/lib/core/utils.js similarity index 100% rename from lib/core/utils.js rename to src/lib/core/utils.js diff --git a/lib/egg.js b/src/lib/egg.ts similarity index 67% rename from lib/egg.js rename to src/lib/egg.ts index aabba225b4..d35aefb025 100644 --- a/lib/egg.js +++ b/src/lib/egg.ts @@ -1,37 +1,92 @@ -const { performance } = require('perf_hooks'); -const path = require('path'); -const fs = require('fs'); -const ms = require('ms'); -const http = require('http'); -const EggCore = require('egg-core').EggCore; -const cluster = require('cluster-client'); -const extend = require('extend2'); -const ContextLogger = require('egg-logger').EggContextLogger; -const ContextCookies = require('egg-cookies'); -const CircularJSON = require('circular-json-for-egg'); -const ContextHttpClient = require('./core/context_httpclient'); -const Messenger = require('./core/messenger'); -const DNSCacheHttpClient = require('./core/dnscache_httpclient'); -const HttpClient = require('./core/httpclient'); -const HttpClientNext = require('./core/httpclient_next'); -const createLoggers = require('./core/logger'); -const Singleton = require('./core/singleton'); -const utils = require('./core/utils'); -const BaseContextClass = require('./core/base_context_class'); -const BaseHookClass = require('./core/base_hook_class'); - -const HTTPCLIENT = Symbol('EggApplication#httpclient'); -const LOGGERS = Symbol('EggApplication#loggers'); +import { performance } from 'node:perf_hooks'; +import path from 'node:path'; +import fs from 'node:fs'; +import ms from 'ms'; +import http, { type IncomingMessage, type ServerResponse } from 'node:http'; +import inspector from 'node:inspector'; +import { EggCore, type EggCoreContext, type EggCoreOptions } from '@eggjs/core'; +import cluster from 'cluster-client'; +import extend from 'extend2'; +import { EggContextLogger as ContextLogger } from 'egg-logger'; +import { Cookies as ContextCookies } from '@eggjs/cookies'; +import CircularJSON from 'circular-json-for-egg'; +import ContextHttpClient from './core/context_httpclient'; +import Messenger from './core/messenger'; +import { + HttpClient, type HttpClientRequestOptions, type HttpClientRequestURL, type HttpClientResponse, +} from './core/httpclient.js'; +import { createLoggers, type EggLoggers, type EggLogger } from './core/logger'; +import { + Singleton, type SingletonCreateMethod, type SingletonOptions, +} from './core/singleton.js'; +import utils from './core/utils'; +import { BaseContextClass } from './core/base_context_class.js'; +import { BaseHookClass } from './core/base_hook_class.js'; + const EGG_PATH = Symbol.for('egg#eggPath'); const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); +export interface EggApplicationOptions extends EggCoreOptions { + mode?: 'cluster' | 'single'; + clusterPort?: number; +} + /** * Based on koa's Application * @see https://github.com/eggjs/egg-core - * @see http://koajs.com/#application + * @see https://github.com/eggjs/koa/blob/master/src/application.ts * @augments EggCore */ -class EggApplication extends EggCore { +export class EggApplication extends EggCore { + // export context base classes, let framework can impl sub class and over context extend easily. + ContextCookies = ContextCookies; + ContextLogger = ContextLogger; + ContextHttpClient = ContextHttpClient; + HttpClient = HttpClient; + /** + * Retrieve base context class + * @member {BaseContextClass} BaseContextClass + * @since 1.0.0 + */ + BaseContextClass = BaseContextClass; + + /** + * Retrieve base controller + * @member {Controller} Controller + * @since 1.0.0 + */ + Controller = BaseContextClass; + + /** + * Retrieve base service + * @member {Service} Service + * @since 1.0.0 + */ + Service = BaseContextClass; + + /** + * Retrieve base subscription + * @member {Subscription} Subscription + * @since 2.12.0 + */ + Subscription = BaseContextClass; + + /** + * Retrieve base context class + * @member {BaseHookClass} BaseHookClass + */ + BaseHookClass = BaseHookClass; + + /** + * Retrieve base boot + * @member {Boot} + */ + Boot = BaseHookClass; + + options: EggApplicationOptions; + + #httpClient?: HttpClient; + #loggers?: EggLoggers; /** * @class @@ -41,17 +96,15 @@ class EggApplication extends EggCore { * - {Object} [plugins] - custom plugin config, use it in unittest * - {String} [mode] - process mode, can be cluster / single, default is `cluster` */ - constructor(options = {}) { - options.mode = options.mode || 'cluster'; + constructor(options?: EggApplicationOptions) { + options = { + mode: 'cluster', + type: 'application', + baseDir: process.cwd(), + ...options, + }; super(options); - // export context base classes, let framework can impl sub class and over context extend easily. - this.ContextCookies = ContextCookies; - this.ContextLogger = ContextLogger; - this.ContextHttpClient = ContextHttpClient; - this.HttpClient = HttpClient; - this.HttpClientNext = HttpClientNext; - this.loader.loadConfig(); /** @@ -75,7 +128,7 @@ class EggApplication extends EggCore { this.dumpTiming(); this.coreLogger.info('[egg:core] dump config after ready, %s', ms(Date.now() - dumpStartTime)); })); - this._setupTimeoutTimer(); + this.#setupTimeoutTimer(); this.console.info('[egg:core] App root: %s', this.baseDir); this.console.info('[egg:core] All *.log files save on %j', this.config.logger.dir); @@ -86,41 +139,6 @@ class EggApplication extends EggCore { process.on('unhandledRejection', this._unhandledRejectionHandler); this[CLUSTER_CLIENTS] = []; - - /** - * Wrap the Client with Leader/Follower Pattern - * - * @description almost the same as Agent.cluster API, the only different is that this method create Follower. - * - * @see https://github.com/node-modules/cluster-client - * @param {Function} clientClass - client class function - * @param {Object} [options] - * - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true - * - {Function} [formatKey] - a method to tranform the subscription info into a string,default is JSON.stringify - * - {Object} [transcode|JSON.stringify/parse] - * - {Function} encode - custom serialize method - * - {Function} decode - custom deserialize method - * - {Boolean} [isBroadcast] - whether broadcast subscrption result to all followers or just one, default is true - * - {Number} [responseTimeout] - response timeout, default is 3 seconds - * - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds - * @return {ClientWrapper} wrapper - */ - this.cluster = (clientClass, options) => { - options = Object.assign({}, this.config.clusterClient, options, { - singleMode: this.options.mode === 'single', - // cluster need a port that can't conflict on the environment - port: this.options.clusterPort, - // agent worker is leader, app workers are follower - isLeader: this.type === 'agent', - logger: this.coreLogger, - // debug mode does not check heartbeat - isCheckHeartbeat: this.config.env === 'prod' ? true : require('inspector').url() === undefined, - }); - const client = cluster(clientClass, options); - this._patchClusterClient(client); - return client; - }; - // register close function this.beforeClose(async () => { // single process mode will close agent before app close @@ -134,46 +152,42 @@ class EggApplication extends EggCore { this.messenger.close(); process.removeListener('unhandledRejection', this._unhandledRejectionHandler); }); + } - /** - * Retreive base context class - * @member {BaseContextClass} BaseContextClass - * @since 1.0.0 - */ - this.BaseContextClass = BaseContextClass; - - /** - * Retreive base controller - * @member {Controller} Controller - * @since 1.0.0 - */ - this.Controller = BaseContextClass; - - /** - * Retreive base service - * @member {Service} Service - * @since 1.0.0 - */ - this.Service = BaseContextClass; - - /** - * Retreive base subscription - * @member {Subscription} Subscription - * @since 2.12.0 - */ - this.Subscription = BaseContextClass; - - /** - * Retreive base context class - * @member {BaseHookClass} BaseHookClass - */ - this.BaseHookClass = BaseHookClass; - - /** - * Retreive base boot - * @member {Boot} - */ - this.Boot = BaseHookClass; + /** + * Wrap the Client with Leader/Follower Pattern + * + * @description almost the same as Agent.cluster API, the only different is that this method create Follower. + * + * @see https://github.com/node-modules/cluster-client + * @param {Function} clientClass - client class function + * @param {Object} [options] + * - {Boolean} [autoGenerate] - whether generate delegate rule automatically, default is true + * - {Function} [formatKey] - a method to transform the subscription info into a string,default is JSON.stringify + * - {Object} [transcode|JSON.stringify/parse] + * - {Function} encode - custom serialize method + * - {Function} decode - custom deserialize method + * - {Boolean} [isBroadcast] - whether broadcast subscription result to all followers or just one, default is true + * - {Number} [responseTimeout] - response timeout, default is 3 seconds + * - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds + * @return {ClientWrapper} wrapper + */ + cluster(clientClass, options) { + options = { + ...this.config.clusterClient, + ...options, + singleMode: this.options.mode === 'single', + // cluster need a port that can't conflict on the environment + port: this.options.clusterPort, + // agent worker is leader, app workers are follower + isLeader: this.type === 'agent', + logger: this.coreLogger, + // debug mode does not check heartbeat + isCheckHeartbeat: this.config.env === 'prod' ? true : inspector.url() === undefined, + }; + const client = cluster(clientClass, options); + this._patchClusterClient(client); + return client; } /** @@ -185,7 +199,7 @@ class EggApplication extends EggCore { * console.log(app); * => * { - * name: 'mockapp', + * name: 'mock-app', * env: 'test', * subdomainOffset: 2, * config: '', @@ -197,14 +211,13 @@ class EggApplication extends EggCore { * } * ``` */ - inspect() { + inspect(): any { const res = { env: this.config.env, }; function delegate(res, app, keys) { for (const key of keys) { - /* istanbul ignore else */ if (app[key]) { res[key] = app[key]; } @@ -213,7 +226,6 @@ class EggApplication extends EggCore { function abbr(res, app, keys) { for (const key of keys) { - /* istanbul ignore else */ if (app[key]) { res[key] = ``; } @@ -254,9 +266,9 @@ class EggApplication extends EggCore { * - method {String} - Request method, defaults to GET. Could be GET, POST, DELETE or PUT. Alias 'type'. * - data {Object} - Data to be sent. Will be stringify automatically. * - dataType {String} - String - Type of response data. Could be `text` or `json`. - * If it's `text`, the callbacked data would be a String. + * If it's `text`, the callback data would be a String. * If it's `json`, the data of callback would be a parsed JSON Object. - * Default callbacked data would be a Buffer. + * Default callback data would be a Buffer. * - headers {Object} - Request headers. * - timeout {Number} - Request timeout in milliseconds. Defaults to exports.TIMEOUT. * Include remote server connecting timeout and response timeout. @@ -266,10 +278,10 @@ class EggApplication extends EggCore { * - gzip {Boolean} - let you get the res object when request connected, default false. alias customResponse * - nestedQuerystring {Boolean} - urllib default use querystring to stringify form data which don't * support nested object, will use qs instead of querystring to support nested object by set this option to true. - * - more options see https://www.npmjs.com/package/urllib + * - more options see https://github.com/node-modules/urllib * @return {Object} * - status {Number} - HTTP response status - * - headers {Object} - HTTP response seaders + * - headers {Object} - HTTP response headers * - res {Object} - HTTP response meta * - data {Object} - HTTP response body * @@ -282,8 +294,8 @@ class EggApplication extends EggCore { * console.log(result.status, result.headers, result.data); * ``` */ - async curl(url, opts) { - return await this.httpclient.request(url, opts); + async curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions): Promise> { + return await this.httpClient.request(url, options); } /** @@ -291,37 +303,32 @@ class EggApplication extends EggCore { * @see https://github.com/node-modules/urllib * @member {HttpClient} */ - get httpclient() { - if (!this[HTTPCLIENT]) { - if (this.config.httpclient.useHttpClientNext) { - this[HTTPCLIENT] = new this.HttpClientNext(this); - } else if (this.config.httpclient.enableDNSCache) { - this[HTTPCLIENT] = new DNSCacheHttpClient(this); - } else { - this[HTTPCLIENT] = new this.HttpClient(this); - } + get httpClient() { + if (!this.#httpClient) { + this.#httpClient = new this.HttpClient(this); } - return this[HTTPCLIENT]; + return this.#httpClient; } /** - * @alias httpclient + * @deprecated please use httpClient instead + * @alias httpClient * @member {HttpClient} */ - get httpClient() { - return this.httpclient; + get httpclient() { + return this.httpClient; } /** - * All loggers contain logger, coreLogger and customLogger + * All loggers contain logger, coreLogger and customLogger * @member {Object} * @since 1.0.0 */ get loggers() { - if (!this[LOGGERS]) { - this[LOGGERS] = createLoggers(this); + if (!this.#loggers) { + this.#loggers = createLoggers(this); } - return this[LOGGERS]; + return this.#loggers; } /** @@ -330,7 +337,7 @@ class EggApplication extends EggCore { * @param {String} name - logger name * @return {Logger} logger */ - getLogger(name) { + getLogger(name: string): EggLogger { return this.loggers[name] || null; } @@ -352,7 +359,7 @@ class EggApplication extends EggCore { return this.getLogger('coreLogger'); } - _unhandledRejectionHandler(err) { + _unhandledRejectionHandler(err: any) { if (!(err instanceof Error)) { const newError = new Error(String(err)); // err maybe an object, try to copy the name, message and stack to the new error instance @@ -374,21 +381,23 @@ class EggApplication extends EggCore { /** * dump out the config and meta object * @private - * @return {Object} the result */ dumpConfigToObject() { - let ignoreList; + let ignoreList: string[]; try { // support array and set ignoreList = Array.from(this.config.dump.ignore); } catch (_) { ignoreList = []; } - - const json = extend(true, {}, { config: this.config, plugins: this.loader.allPlugins, appInfo: this.loader.appInfo }); - utils.convertObject(json, ignoreList); + const config = extend(true, {}, { + config: this.config, + plugins: this.loader.allPlugins, + appInfo: this.loader.appInfo, + }); + utils.convertObject(config, ignoreList); return { - config: json, + config, meta: this.loader.configMeta, }; } @@ -403,7 +412,7 @@ class EggApplication extends EggCore { /* istanbul ignore if */ if (!fs.existsSync(rundir)) fs.mkdirSync(rundir); - // get dumpped object + // get dumped object const { config, meta } = this.dumpConfigToObject(); // dump config @@ -427,10 +436,10 @@ class EggApplication extends EggCore { this.coreLogger.info(this.timing.toString()); // only disable, not clear bootstrap timing data. this.timing.disable(); - // show duration >= ${slowBootActionMinDuration}ms action to warnning log + // show duration >= ${slowBootActionMinDuration}ms action to warning log for (const item of items) { // ignore #0 name: Process Start - if (item.index > 0 && item.duration >= this.config.dump.timing.slowBootActionMinDuration) { + if (item.index > 0 && item.duration && item.duration >= this.config.dump.timing.slowBootActionMinDuration) { this.coreLogger.warn('[egg:core][slow-boot-action] #%d %dms, name: %s', item.index, item.duration, item.name); } @@ -444,7 +453,7 @@ class EggApplication extends EggCore { return path.join(__dirname, '..'); } - _setupTimeoutTimer() { + #setupTimeoutTimer() { const startTimeoutTimer = setTimeout(() => { this.coreLogger.error(this.timing.toString()); this.coreLogger.error(`${this.type} still doesn't ready after ${this.config.workerStartTimeout} ms.`); @@ -489,11 +498,12 @@ class EggApplication extends EggCore { * @param {String} name - unique name for singleton * @param {Function|AsyncFunction} create - method will be invoked when singleton instance create */ - addSingleton(name, create) { - const options = {}; - options.name = name; - options.create = create; - options.app = this; + addSingleton(name: string, create: SingletonCreateMethod) { + const options: SingletonOptions = { + name, + create, + app: this, + }; const singleton = new Singleton(options); const initPromise = singleton.init(); if (initPromise) { @@ -520,7 +530,7 @@ class EggApplication extends EggCore { * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. * @return {Context} context */ - createAnonymousContext(req) { + createAnonymousContext(req?: any) { const request = { headers: { host: '127.0.0.1', @@ -539,7 +549,7 @@ class EggApplication extends EggCore { remoteAddress: '127.0.0.1', remotePort: 7001, }, - }; + } as unknown as IncomingMessage; if (req) { for (const key in req) { if (key === 'headers' || key === 'query' || key === 'socket') { @@ -560,12 +570,11 @@ class EggApplication extends EggCore { * @param {Res} res - node native Response object * @return {Context} context object */ - createContext(req, res) { - const app = this; - const context = Object.create(app.context); - const request = context.request = Object.create(app.request); - const response = context.response = Object.create(app.response); - context.app = request.app = response.app = app; + createContext(req: IncomingMessage, res: ServerResponse) { + 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; @@ -579,7 +588,6 @@ class EggApplication extends EggCore { * @member {Number} Context#starttime */ context.starttime = Date.now(); - if (this.config.logger.enablePerformanceTimer) { /** * Request start timer using `performance.now()` @@ -587,9 +595,6 @@ class EggApplication extends EggCore { */ context.performanceStarttime = performance.now(); } - return context; + return context as unknown as EggCoreContext; } - } - -module.exports = EggApplication; diff --git a/lib/loader/agent_worker_loader.js b/src/lib/loader/agent_worker_loader.js similarity index 100% rename from lib/loader/agent_worker_loader.js rename to src/lib/loader/agent_worker_loader.js diff --git a/lib/loader/app_worker_loader.js b/src/lib/loader/app_worker_loader.js similarity index 100% rename from lib/loader/app_worker_loader.js rename to src/lib/loader/app_worker_loader.js diff --git a/lib/loader/index.js b/src/lib/loader/index.js similarity index 100% rename from lib/loader/index.js rename to src/lib/loader/index.js diff --git a/lib/start.js b/src/lib/start.js similarity index 100% rename from lib/start.js rename to src/lib/start.js diff --git a/src/lib/type.ts b/src/lib/type.ts new file mode 100644 index 0000000000..3fa700921c --- /dev/null +++ b/src/lib/type.ts @@ -0,0 +1,330 @@ +import type { Socket } from 'node:net'; +import type { EggCoreContext } from '@eggjs/core'; +import type { + RequestOptions as HttpClientRequestOptions, +} from 'urllib'; +import type { + EggLoggerOptions, EggLoggersOptions, +} from 'egg-logger'; +import type { + FileLoaderOptions, +} from '@eggjs/core'; +import type { + EggApplication, +} from './egg.js'; +import type { MetaMiddlewareOptions } from '../app/middleware/meta.js'; +import type { NotFoundMiddlewareOptions } from '../app/middleware/notfound.js'; +import type { SiteFileMiddlewareOptions } from '../app/middleware/site_file.js'; + +type IgnoreItem = string | RegExp | ((ctx: EggCoreContext) => boolean); +type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; + +export type Next = () => Promise; + +export interface ClientErrorResponse { + body: string | Buffer; + status: number; + headers: { [key: string]: string }; +} + +/** egg env type */ +export type EggEnvType = 'local' | 'unittest' | 'prod' | string; + +/** logger config of egg */ +interface EggLoggerConfig extends Omit { + /** custom config of coreLogger */ + coreLogger?: Partial; + /** allow debug log at prod, defaults to `false` */ + allowDebugAtProd?: boolean; + /** disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. */ + disableConsoleAfterReady?: boolean; + /** using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to `false`. e.g.: logger will set 1.456ms instead of 1ms. */ + enablePerformanceTimer?: boolean; + /** using the app logger instead of EggContextLogger, defaults to `false` */ + enableFastContextLogger?: boolean; +} + +/** Custom Loader Configuration */ +export interface CustomLoaderConfig extends Omit { + /** + * an object you wanner load to, value can only be 'ctx' or 'app'. default to app + */ + inject?: 'ctx' | 'app'; + /** + * whether need to load files in plugins or framework, default to false + */ + loadunit?: boolean; +} + +export interface EggAppConfig { + workerStartTimeout: number; + baseDir: string; + middleware: string[]; + coreMiddleware: string[]; + + /** + * The option of `bodyParser` middleware + * + * @member Config#bodyParser + * @property {Boolean} enable - enable bodyParser or not, default to true + * @property {String | RegExp | Function | Array} ignore - won't parse request body when url path hit ignore pattern, can not set `ignore` when `match` presented + * @property {String | RegExp | Function | Array} match - will parse request body only when url path hit match pattern + * @property {String} encoding - body encoding config, default utf8 + * @property {String} formLimit - form body size limit, default 1mb + * @property {String} jsonLimit - json body size limit, default 1mb + * @property {String} textLimit - json body size limit, default 1mb + * @property {Boolean} strict - json body strict mode, if set strict value true, then only receive object and array json body + * @property {Number} queryString.arrayLimit - from item array length limit, default 100 + * @property {Number} queryString.depth - json value deep length, default 5 + * @property {Number} queryString.parameterLimit - parameter number limit, default 1000 + * @property {String[]} enableTypes - parser will only parse when request type hits enableTypes, default is ['json', 'form'] + * @property {Object} extendTypes - support extend types + * @property {String} onProtoPoisoning - Defines what action must take when parsing a JSON object with `__proto__`. Possible values are `'error'`, `'remove'` and `'ignore'`. Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. + */ + bodyParser: { + enable: boolean; + encoding: string; + formLimit: string; + jsonLimit: string; + textLimit: string; + strict: boolean; + queryString: { + arrayLimit: number; + depth: number; + parameterLimit: number; + }; + ignore?: IgnoreOrMatch; + match?: IgnoreOrMatch; + enableTypes?: string[]; + extendTypes?: { + json: string[]; + form: string[]; + text: string[]; + }; + /** Default is `'error'`, it will return `400` response when `Prototype-Poisoning` happen. */ + onProtoPoisoning: 'error' | 'remove' | 'ignore'; + onerror(err: any, ctx: EggCoreContext): void; + }; + + /** + * logger options + * @member Config#logger + * @property {String} dir - directory of log files + * @property {String} encoding - log file encoding, defaults to utf8 + * @property {String} level - default log level, could be: DEBUG, INFO, WARN, ERROR or NONE, defaults to INFO in production + * @property {String} consoleLevel - log level of stdout, defaults to `INFO` in local serverEnv, defaults to `WARN` in unittest, others is `NONE` + * @property {Boolean} disableConsoleAfterReady - disable logger console after app ready. defaults to `false` on local and unittest env, others is `true`. + * @property {Boolean} outputJSON - log as JSON or not, defaults to `false` + * @property {Boolean} buffer - if enabled, flush logs to disk at a certain frequency to improve performance, defaults to true + * @property {String} errorLogName - file name of errorLogger + * @property {String} coreLogName - file name of coreLogger + * @property {String} agentLogName - file name of agent worker log + * @property {Object} coreLogger - custom config of coreLogger + * @property {Boolean} allowDebugAtProd - allow debug log at prod, defaults to false + * @property {Boolean} enablePerformanceTimer - using performance.now() timer instead of Date.now() for more more precise milliseconds, defaults to false. e.g.: logger will set 1.456ms instead of 1ms. + * @property {Boolean} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false + */ + logger: Partial; + + /** custom logger of egg */ + customLogger: { + [key: string]: EggLoggerOptions; + }; + + /** Configuration of httpclient in egg. */ + httpclient: { + /** Request timeout */ + timeout?: number; + /** Default request args for httpclient */ + request?: HttpClientRequestOptions; + }; + + development: { + /** + * dirs needed watch, when files under these change, application will reload, use relative path + */ + watchDirs: string[]; + /** + * dirs don't need watch, including subdirectories, use relative path + */ + ignoreDirs: string[]; + /** + * don't wait all plugins ready, default is true. + */ + fastReady: boolean; + /** + * whether reload on debug, default is true. + */ + reloadOnDebug: boolean; + /** + * whether override default watchDirs, default is false. + */ + overrideDefault: boolean; + /** + * whether override default ignoreDirs, default is false. + */ + overrideIgnore: boolean; + /** + * whether to reload, use https://github.com/sindresorhus/multimatch + */ + reloadPattern: string[] | string; + }; + + /** + * customLoader config + */ + customLoader: { + [key: string]: CustomLoaderConfig; + }; + + /** + * It will ignore special keys when dumpConfig + */ + dump: { + ignore: Set; + timing: { + slowBootActionMinDuration: number; + }; + }; + + /** + * The environment of egg + */ + env: EggEnvType; + + /** + * The current HOME directory + */ + HOME: string; + + hostHeaders: string; + + /** + * I18n options + */ + i18n: { + /** + * default value EN_US + */ + defaultLocale: string; + /** + * i18n resource file dir, not recommend to change default value + */ + dirs: string[]; + /** + * custom the locale value field, default `query.locale`, you can modify this config, such as `query.lang` + */ + queryField: string; + /** + * The locale value key in the cookie, default is locale. + */ + cookieField: string; + /** + * Locale cookie expire time, default `1y`, If pass number value, the unit will be ms + */ + cookieMaxAge: string | number; + }; + + /** + * Detect request' ip from specified headers, not case-sensitive. Only worked when config.proxy set to true. + */ + ipHeaders: string; + + protocolHeaders: string; + maxProxyCount: number; + maxIpsCount: number; + proxy: boolean; + cookies: { + sameSite?: string; + httpOnly?: boolean; + }; + + /** + * jsonp options + * @member Config#jsonp + * @property {String} callback - jsonp callback method key, default to `_callback` + * @property {Number} limit - callback method name's max length, default to `50` + * @property {Boolean} csrf - enable csrf check or not. default to false + * @property {String|RegExp|Array} whiteList - referrer white list + */ + jsonp: { + limit: number; + callback: string; + csrf: boolean; + whiteList: string | RegExp | Array; + }; + + /** + * The key that signing cookies. It can contain multiple keys separated by . + */ + keys: string; + + /** + * The name of the application + */ + name: string; + + /** + * package.json + */ + pkg: Record; + + rundir: string; + + security: { + domainWhiteList: string[]; + protocolWhiteList: string[]; + defaultMiddleware: string; + csrf: any; + ssrf: { + ipBlackList: string[]; + ipExceptionList: string[]; + checkAddress?(ip: string): boolean; + }; + xframe: { + enable: boolean; + value: 'SAMEORIGIN' | 'DENY' | 'ALLOW-FROM'; + }; + hsts: any; + methodnoallow: { enable: boolean }; + noopen: { enable: boolean; } + xssProtection: any; + csp: any; + }; + + siteFile: SiteFileMiddlewareOptions; + meta: MetaMiddlewareOptions; + notfound: NotFoundMiddlewareOptions; + overrideMethod: { + enable: boolean; + allowedMethods: string[]; + }; + + watcher: Record; + + onClientError?(err: Error, socket: Socket, app: EggApplication): ClientErrorResponse | Promise; + + /** + * server timeout in milliseconds, default to 0 (no timeout). + * + * for special request, just use `ctx.req.setTimeout(ms)` + * + * @see https://nodejs.org/api/http.html#http_server_timeout + */ + serverTimeout: number | null; + + cluster: { + listen: { + path: string, + port: number, + hostname: string, + }; + }; + + clusterClient: { + maxWaitTime: number; + responseTimeout: number; + }; + + [prop: string]: any; +}