From 7232a5fbf6888a59ca4aca0329ee3424a1555958 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 18 Jun 2024 09:25:18 +0800 Subject: [PATCH 01/12] feat: refactor with typescript to support cjs ane esm both BREAKING CHANGE: Drop Node.js < 18.19.0 support part of https://github.com/eggjs/egg/issues/3644 Breaking changes: - Drop Node.js < 18.19.0 support - Drop generator function support use @eggjs/core@4 https://github.com/eggjs/egg-core/pull/265 --- .github/PULL_REQUEST_TEMPLATE.md | 29 ------------ .github/workflows/codeql-analysis.yml | 68 --------------------------- .github/workflows/nodejs.yml | 10 ++-- .github/workflows/release.yml | 5 +- package.json | 10 ++-- 5 files changed, 12 insertions(+), 110 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 999b42b72a..0000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,29 +0,0 @@ - - -##### Checklist - - -- [ ] `npm test` passes -- [ ] tests and/or benchmarks are included -- [ ] documentation is changed or added -- [ ] commit message follows commit guidelines - -##### Affected core subsystem(s) - - - -##### Description of change - - - \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 0e318168e0..0000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,68 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master, 1.x ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 68ad9c1ac4..c70132dabd 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -2,10 +2,9 @@ name: CI on: push: - branches: [ master, 2.x, 1.x ] - + branches: [ master ] pull_request: - branches: [ master, 2.x, 1.x ] + branches: [ master ] jobs: Job: @@ -13,5 +12,6 @@ jobs: uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: os: 'ubuntu-latest, macos-latest, windows-latest' - version: '14, 16, 18, 20, 22' - install: 'npm i -g npminstall && npminstall' + version: '18.19.0, 18, 20, 22' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8fbb48f9f..a2bf04a759 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ name: Release + on: push: - branches: [ master, 2.x, 1.x ] + branches: [ master ] jobs: release: @@ -10,5 +11,3 @@ jobs: secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} - with: - install: 'npm install --legacy-peer-deps --no-package-lock --no-fund' diff --git a/package.json b/package.json index bbf1bf0cf3..6bb6658052 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "egg", - "version": "3.24.1", + "version": "4.0.0-beta.0", + "engines": { + "node": ">= 18.19.0" + }, "publishConfig": { - "tag": "latest" + "tag": "next" }, "description": "A web framework's framework for Node.js", "keywords": [ @@ -128,8 +131,5 @@ "type": "git", "url": "https://github.com/eggjs/egg.git" }, - "engines": { - "node": ">= 14.20.0" - }, "license": "MIT" } From 89df72a6345de070d9adc3a8be2d2072cbd651b9 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 23 Jun 2024 23:44:50 +0800 Subject: [PATCH 02/12] middleware --- .eslintrc | 5 +- .github/dependabot.yml | 7 - .github/workflows/gh-pages.yml | 2 +- README.md | 4 +- README.zh-CN.md | 11 +- app/middleware/body_parser.js | 3 - app/middleware/override_method.js | 3 - app/middleware/site_file.js | 31 -- config/config.local.js | 7 - config/config.unittest.js | 8 - lib/core/dnscache_httpclient.js | 93 ----- lib/core/httpclient.js | 108 ------ lib/core/httpclient_next.js | 37 -- package.json | 118 +++--- scripts/commits.sh | 9 - agent.js => src/agent.js | 0 {app => src/app}/extend/context.js | 0 {app => src/app}/extend/helper.js | 0 {app => src/app}/extend/request.js | 0 {app => src/app}/extend/response.js | 0 src/app/middleware/body_parser.ts | 3 + .../meta.js => src/app/middleware/meta.ts | 16 +- .../app/middleware/notfound.ts | 12 +- src/app/middleware/override_method.ts | 3 + src/app/middleware/site_file.ts | 49 +++ .../config/config.default.ts | 63 ++-- src/config/config.local.ts | 11 + src/config/config.unittest.ts | 10 + {config => src/config}/favicon.png | Bin config/plugin.js => src/config/plugin.ts | 4 +- index.d.ts => src/index.d.ts | 0 index.js => src/index.js | 0 index.test-d.ts => src/index.test-d.ts | 0 {lib => src/lib}/agent.js | 0 {lib => src/lib}/application.js | 0 .../lib/core/base_context_class.ts | 12 +- .../lib/core/base_context_logger.ts | 31 +- .../lib/core/base_hook_class.ts | 8 +- {lib => src/lib}/core/context_httpclient.js | 0 src/lib/core/httpclient.ts | 49 +++ lib/core/logger.js => src/lib/core/logger.ts | 15 +- {lib => src/lib}/core/messenger/index.js | 0 {lib => src/lib}/core/messenger/ipc.js | 0 {lib => src/lib}/core/messenger/local.js | 0 .../singleton.js => src/lib/core/singleton.ts | 67 ++-- {lib => src/lib}/core/utils.js | 0 lib/egg.js => src/lib/egg.ts | 339 +++++++++--------- .../lib}/loader/agent_worker_loader.js | 0 {lib => src/lib}/loader/app_worker_loader.js | 0 {lib => src/lib}/loader/index.js | 0 {lib => src/lib}/start.js | 0 src/lib/type.ts | 330 +++++++++++++++++ 52 files changed, 830 insertions(+), 638 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 app/middleware/body_parser.js delete mode 100644 app/middleware/override_method.js delete mode 100644 app/middleware/site_file.js delete mode 100644 config/config.local.js delete mode 100644 config/config.unittest.js delete mode 100644 lib/core/dnscache_httpclient.js delete mode 100644 lib/core/httpclient.js delete mode 100644 lib/core/httpclient_next.js delete mode 100755 scripts/commits.sh rename agent.js => src/agent.js (100%) rename {app => src/app}/extend/context.js (100%) rename {app => src/app}/extend/helper.js (100%) rename {app => src/app}/extend/request.js (100%) rename {app => src/app}/extend/response.js (100%) create mode 100644 src/app/middleware/body_parser.ts rename app/middleware/meta.js => src/app/middleware/meta.ts (51%) rename app/middleware/notfound.js => src/app/middleware/notfound.ts (69%) create mode 100644 src/app/middleware/override_method.ts create mode 100644 src/app/middleware/site_file.ts rename config/config.default.js => src/config/config.default.ts (92%) create mode 100644 src/config/config.local.ts create mode 100644 src/config/config.unittest.ts rename {config => src/config}/favicon.png (100%) rename config/plugin.js => src/config/plugin.ts (98%) rename index.d.ts => src/index.d.ts (100%) rename index.js => src/index.js (100%) rename index.test-d.ts => src/index.test-d.ts (100%) rename {lib => src/lib}/agent.js (100%) rename {lib => src/lib}/application.js (100%) rename lib/core/base_context_class.js => src/lib/core/base_context_class.ts (59%) rename lib/core/base_context_logger.js => src/lib/core/base_context_logger.ts (62%) rename lib/core/base_hook_class.js => src/lib/core/base_hook_class.ts (83%) rename {lib => src/lib}/core/context_httpclient.js (100%) create mode 100644 src/lib/core/httpclient.ts rename lib/core/logger.js => src/lib/core/logger.ts (65%) rename {lib => src/lib}/core/messenger/index.js (100%) rename {lib => src/lib}/core/messenger/ipc.js (100%) rename {lib => src/lib}/core/messenger/local.js (100%) rename lib/core/singleton.js => src/lib/core/singleton.ts (66%) rename {lib => src/lib}/core/utils.js (100%) rename lib/egg.js => src/lib/egg.ts (67%) rename {lib => src/lib}/loader/agent_worker_loader.js (100%) rename {lib => src/lib}/loader/app_worker_loader.js (100%) rename {lib => src/lib}/loader/index.js (100%) rename {lib => src/lib}/start.js (100%) create mode 100644 src/lib/type.ts 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; +} From 45647f570fd0e0fb310c20d133a6e3bf8be75e06 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 14:09:13 +0800 Subject: [PATCH 03/12] support EGG_PATH --- package.json | 10 ++- src/lib/{agent.js => agent.ts} | 18 +---- src/lib/{application.js => application.ts} | 32 +++----- src/lib/core/messenger/IMessenger.ts | 56 ++++++++++++++ src/lib/core/messenger/index.js | 14 ---- src/lib/core/messenger/index.ts | 15 ++++ src/lib/core/messenger/{ipc.js => ipc.ts} | 48 ++++++------ src/lib/core/messenger/{local.js => local.ts} | 42 +++++----- src/lib/core/utils.js | 73 ------------------ src/lib/core/utils.ts | 77 +++++++++++++++++++ src/lib/egg.ts | 56 +++++++++----- src/lib/start.js | 39 ---------- src/lib/start.ts | 51 ++++++++++++ 13 files changed, 305 insertions(+), 226 deletions(-) rename src/lib/{agent.js => agent.ts} (85%) rename src/lib/{application.js => application.ts} (94%) create mode 100644 src/lib/core/messenger/IMessenger.ts delete mode 100644 src/lib/core/messenger/index.js create mode 100644 src/lib/core/messenger/index.ts rename src/lib/core/messenger/{ipc.js => ipc.ts} (73%) rename src/lib/core/messenger/{local.js => local.ts} (76%) delete mode 100644 src/lib/core/utils.js create mode 100644 src/lib/core/utils.ts delete mode 100644 src/lib/start.js create mode 100644 src/lib/start.ts diff --git a/package.json b/package.json index bedc098374..cdef1ad522 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ ], "dependencies": { "@eggjs/cookies": "^3.0.0", - "@eggjs/core": "^6.0.1", + "@eggjs/core": "^6.0.2", + "@eggjs/utils": "^4.0.2", "@types/accepts": "^1.3.5", "accepts": "^1.3.8", "cache-content-type": "^2.0.0", @@ -51,7 +52,7 @@ "ms": "^2.1.3", "on-finished": "^2.4.1", "onelogger": "^1.0.0", - "sendmessage": "^2.0.0", + "sendmessage": "^3.0.0", "urllib": "^4.0.0", "utility": "^2.1.0", "ylru": "^1.3.2" @@ -76,7 +77,7 @@ "egg-tracer": "^2.1.0", "egg-view-nunjucks": "^2.3.0", "eslint": "8", - "eslint-config-egg": "13", + "eslint-config-egg": "14", "findlinks": "^2.2.0", "formstream": "^1.5.1", "jsdoc": "^3.6.11", @@ -121,6 +122,9 @@ "url": "git://github.com/eggjs/egg.git" }, "license": "MIT", + "tnpm": { + "mode": "npm" + }, "egg": { "framework": true, "baseDir": { diff --git a/src/lib/agent.js b/src/lib/agent.ts similarity index 85% rename from src/lib/agent.js rename to src/lib/agent.ts index bfb3edf59c..a5b71ae7ee 100644 --- a/src/lib/agent.js +++ b/src/lib/agent.ts @@ -1,18 +1,14 @@ -'use strict'; - -const path = require('path'); -const ms = require('ms'); -const EggApplication = require('./egg'); -const AgentWorkerLoader = require('./loader').AgentWorkerLoader; +import ms from 'ms'; +import { EggApplication } from './egg.js'; +import { AgentWorkerLoader } from './loader'; const EGG_LOADER = Symbol.for('egg#loader'); -const EGG_PATH = Symbol.for('egg#eggPath'); /** * Singleton instance in Agent Worker, extend {@link EggApplication} * @augments EggApplication */ -class Agent extends EggApplication { +export class Agent extends EggApplication { /** * @class * @param {Object} options - see {@link EggApplication} @@ -53,10 +49,6 @@ class Agent extends EggApplication { return AgentWorkerLoader; } - get [EGG_PATH]() { - return path.join(__dirname, '..'); - } - _wrapMessenger() { for (const methodName of [ 'broadcast', @@ -91,5 +83,3 @@ class Agent extends EggApplication { return super.close(); } } - -module.exports = Agent; diff --git a/src/lib/application.js b/src/lib/application.ts similarity index 94% rename from src/lib/application.js rename to src/lib/application.ts index eac56c9c0a..6fcbbcc90d 100644 --- a/src/lib/application.js +++ b/src/lib/application.ts @@ -1,17 +1,15 @@ -'use strict'; - -const path = require('path'); -const fs = require('fs'); -const ms = require('ms'); -const is = require('is-type-of'); -const graceful = require('graceful'); -const http = require('http'); -const cluster = require('cluster-client'); -const onFinished = require('on-finished'); -const { assign } = require('utility'); +import path from 'path'; +import fs from 'fs'; +import ms from 'ms'; +import is from 'is-type-of'; +import graceful from 'graceful'; +import http from 'http'; +import cluster from 'cluster-client'; +import onFinished from 'on-finished'; +import { assign } from 'utility'; const eggUtils = require('egg-core').utils; -const EggApplication = require('./egg'); -const AppWorkerLoader = require('./loader').AppWorkerLoader; +import { EggApplication } from './egg.js'; +import { AppWorkerLoader } from './loader'; const KEYS = Symbol('Application#keys'); const HELPER = Symbol('Application#Helper'); @@ -19,7 +17,6 @@ const LOCALS = Symbol('Application#locals'); const BIND_EVENTS = Symbol('Application#bindEvents'); const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig'); const EGG_LOADER = Symbol.for('egg#loader'); -const EGG_PATH = Symbol.for('egg#eggPath'); const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); const RESPONSE_RAW = Symbol('Application#responseRaw'); @@ -49,7 +46,7 @@ function escapeHeaderValue(value) { * Singleton instance in App Worker, extend {@link EggApplication} * @augments EggApplication */ -class Application extends EggApplication { +export class Application extends EggApplication { /** * @class @@ -83,10 +80,6 @@ class Application extends EggApplication { return AppWorkerLoader; } - get [EGG_PATH]() { - return path.join(__dirname, '..'); - } - [RESPONSE_RAW](socket, raw) { /* istanbul ignore next */ if (!socket.writable) return; @@ -345,4 +338,3 @@ class Application extends EggApplication { } } -module.exports = Application; diff --git a/src/lib/core/messenger/IMessenger.ts b/src/lib/core/messenger/IMessenger.ts new file mode 100644 index 0000000000..1c749a3a99 --- /dev/null +++ b/src/lib/core/messenger/IMessenger.ts @@ -0,0 +1,56 @@ +import type { EventEmitter } from 'node:events'; + +export interface IMessenger extends EventEmitter { + /** + * Send message to all agent and app + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + broadcast(action: string, data?: unknown): IMessenger; + + /** + * send message to the specified process + * @param {String} pid - the process id of the receiver + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendTo(pid: string, action: string, data?: unknown): IMessenger; + + /** + * send message to one app worker by random + * - if it's running in agent, it will send to one of app workers + * - if it's running in app, it will send to agent + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendRandom(action: string, data?: unknown): IMessenger; + + /** + * send message to app + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToApp(action: string, data?: unknown): IMessenger; + + /** + * send message to agent + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToAgent(action: string, data?: unknown): IMessenger; + + /** + * @param {String} action - message key + * @param {Object} data - message value + * @param {String} to - let master know how to send message + * @return {Messenger} this + */ + send(action: string, data: unknown | undefined, to: string): IMessenger; + + close(): void; +} diff --git a/src/lib/core/messenger/index.js b/src/lib/core/messenger/index.js deleted file mode 100644 index f819cfa917..0000000000 --- a/src/lib/core/messenger/index.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -const LocalMessenger = require('./local'); -const IPCMessenger = require('./ipc'); - -/** - * @class Messenger - */ - -exports.create = egg => { - return egg.options.mode === 'single' - ? new LocalMessenger(egg) - : new IPCMessenger(egg); -}; diff --git a/src/lib/core/messenger/index.ts b/src/lib/core/messenger/index.ts new file mode 100644 index 0000000000..9c283c5a40 --- /dev/null +++ b/src/lib/core/messenger/index.ts @@ -0,0 +1,15 @@ +import { Messenger as LocalMessenger } from './local.js'; +import { Messenger as IPCMessenger } from './ipc.js'; +import type { IMessenger } from './IMessenger.js'; +import type { EggApplication } from '../../egg.js'; + +export type { IMessenger } from './IMessenger.js'; + +/** + * @class Messenger + */ +export function create(egg: EggApplication): IMessenger { + return egg.options.mode === 'single' + ? new LocalMessenger(egg) + : new IPCMessenger(); +} diff --git a/src/lib/core/messenger/ipc.js b/src/lib/core/messenger/ipc.ts similarity index 73% rename from src/lib/core/messenger/ipc.js rename to src/lib/core/messenger/ipc.ts index e758d6dd2c..8bfb1a44a4 100644 --- a/src/lib/core/messenger/ipc.js +++ b/src/lib/core/messenger/ipc.ts @@ -1,15 +1,18 @@ -'use strict'; -const debug = require('util').debuglog('egg:util:messenger:ipc'); -const is = require('is-type-of'); -const workerThreads = require('worker_threads'); -const sendmessage = require('sendmessage'); -const EventEmitter = require('events'); +import { EventEmitter } from 'node:events'; +import { debuglog } from 'node:util'; +import workerThreads from 'node:worker_threads'; +import sendmessage from 'sendmessage'; +import type { IMessenger } from './IMessenger.js'; + +const debug = debuglog('egg:lib:core:messenger:ipc'); /** * Communication between app worker and agent worker by IPC channel */ -class Messenger extends EventEmitter { +export class Messenger extends EventEmitter implements IMessenger { + readonly pid: string; + opids: string[] = []; constructor() { super(); @@ -17,14 +20,13 @@ class Messenger extends EventEmitter { // pids of agent or app managed by master // - retrieve app worker pids when it's an agent worker // - retrieve agent worker pids when it's an app worker - this.opids = []; this.on('egg-pids', pids => { this.opids = pids; }); this._onMessage = this._onMessage.bind(this); process.on('message', this._onMessage); if (!workerThreads.isMainThread) { - workerThreads.parentPort.on('message', this._onMessage); + workerThreads.parentPort!.on('message', this._onMessage); } } @@ -34,7 +36,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - broadcast(action, data) { + broadcast(action: string, data?: unknown): Messenger { debug('[%s] broadcast %s with %j', this.pid, action, data); this.send(action, data, 'app'); this.send(action, data, 'agent'); @@ -48,7 +50,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid, action, data) { + sendTo(pid: string, action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to %s', this.pid, action, data, pid); sendmessage(process, { action, @@ -66,10 +68,11 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendRandom(action, data) { + sendRandom(action: string, data?: unknown): Messenger { /* istanbul ignore if */ - if (!this.opids.length) return this; - const pid = random(this.opids); + if (this.opids.length === 0) return this; + const index = Math.floor(Math.random() * this.opids.length); + const pid = this.opids[index]; this.sendTo(String(pid), action, data); return this; } @@ -80,7 +83,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToApp(action, data) { + sendToApp(action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to all app', this.pid, action, data); this.send(action, data, 'app'); return this; @@ -92,7 +95,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToAgent(action, data) { + sendToAgent(action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to all agent', this.pid, action, data); this.send(action, data, 'agent'); return this; @@ -104,7 +107,7 @@ class Messenger extends EventEmitter { * @param {String} to - let master know how to send message * @return {Messenger} this */ - send(action, data, to) { + send(action: string, data: unknown | undefined, to: string): Messenger { sendmessage(process, { action, data, @@ -113,8 +116,8 @@ class Messenger extends EventEmitter { return this; } - _onMessage(message) { - if (message && is.string(message.action)) { + _onMessage(message: any) { + if (typeof message?.action === 'string') { debug('[%s] got message %s with %j, receiverPid: %s', this.pid, message.action, message.data, message.receiverPid); this.emit(message.action, message.data); @@ -132,10 +135,3 @@ class Messenger extends EventEmitter { * @param {Object} data - message value */ } - -module.exports = Messenger; - -function random(arr) { - const index = Math.floor(Math.random() * arr.length); - return arr[index]; -} diff --git a/src/lib/core/messenger/local.js b/src/lib/core/messenger/local.ts similarity index 76% rename from src/lib/core/messenger/local.js rename to src/lib/core/messenger/local.ts index 7be992e0ee..462a81c02a 100644 --- a/src/lib/core/messenger/local.js +++ b/src/lib/core/messenger/local.ts @@ -1,17 +1,21 @@ -'use strict'; +import { debuglog } from 'node:util'; +import EventEmitter from 'node:events'; +import type { IMessenger } from './IMessenger.js'; +import type { EggApplication } from '../../egg.js'; -const debug = require('util').debuglog('egg:util:messenger:local'); -const is = require('is-type-of'); -const EventEmitter = require('events'); +const debug = debuglog('egg:lib:core:messenger:local'); /** * Communication between app worker and agent worker with EventEmitter */ -class Messenger extends EventEmitter { +export class Messenger extends EventEmitter implements IMessenger { + readonly pid: string; + readonly egg: EggApplication; - constructor(egg) { + constructor(egg: EggApplication) { super(); this.egg = egg; + this.pid = String(process.pid); } /** @@ -20,7 +24,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - broadcast(action, data) { + broadcast(action: string, data?: unknown): Messenger { debug('[%s] broadcast %s with %j', this.pid, action, data); this.send(action, data, 'both'); return this; @@ -35,9 +39,11 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid, action, data) { + sendTo(pid: string, action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to %s', this.pid, action, data, pid); - if (pid !== process.pid) return this; + if (String(pid) !== this.pid) { + return this; + } this.send(action, data, 'both'); return this; } @@ -51,7 +57,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendRandom(action, data) { + sendRandom(action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to opposite', this.pid, action, data); this.send(action, data, 'opposite'); return this; @@ -63,7 +69,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToApp(action, data) { + sendToApp(action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to all app', this.pid, action, data); this.send(action, data, 'application'); return this; @@ -75,7 +81,7 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToAgent(action, data) { + sendToAgent(action: string, data?: unknown): Messenger { debug('[%s] send %s with %j to all agent', this.pid, action, data); this.send(action, data, 'agent'); return this; @@ -87,7 +93,7 @@ class Messenger extends EventEmitter { * @param {String} to - let master know how to send message * @return {Messenger} this */ - send(action, data, to) { + send(action: string, data: unknown | undefined, to: string): Messenger { // use nextTick to keep it async as IPC messenger process.nextTick(() => { const { egg } = this; @@ -104,7 +110,9 @@ class Messenger extends EventEmitter { application = egg.application; opposite = application; } - if (!to) to = egg.type === 'application' ? 'agent' : 'application'; + if (!to) { + to = egg.type === 'application' ? 'agent' : 'application'; + } if (application && application.messenger && (to === 'application' || to === 'both')) { application.messenger._onMessage({ action, data }); @@ -120,8 +128,8 @@ class Messenger extends EventEmitter { return this; } - _onMessage(message) { - if (message && is.string(message.action)) { + _onMessage(message: any) { + if (typeof message?.action === 'string') { debug('[%s] got message %s with %j', this.pid, message.action, message.data); this.emit(message.action, message.data); } @@ -137,5 +145,3 @@ class Messenger extends EventEmitter { * @param {Object} data - message value */ } - -module.exports = Messenger; diff --git a/src/lib/core/utils.js b/src/lib/core/utils.js deleted file mode 100644 index 445dead72e..0000000000 --- a/src/lib/core/utils.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -const util = require('util'); -const is = require('is-type-of'); -const URL = require('url').URL; - -module.exports = { - convertObject, - safeParseURL, -}; - -function convertObject(obj, ignore) { - if (!is.array(ignore)) ignore = [ ignore ]; - for (const key of Object.keys(obj)) { - obj[key] = convertValue(key, obj[key], ignore); - } - return obj; -} - -function convertValue(key, value, ignore) { - if (is.nullOrUndefined(value)) return value; - - let hit = false; - for (const matchKey of ignore) { - if (is.string(matchKey) && matchKey === key) { - hit = true; - break; - } else if (is.regExp(matchKey) && matchKey.test(key)) { - hit = true; - break; - } - } - if (!hit) { - if (is.symbol(value) || is.regExp(value)) return value.toString(); - if (is.primitive(value) || is.array(value)) return value; - } - - // only convert recursively when it's a plain object, - // o = {} - if (Object.getPrototypeOf(value) === Object.prototype) { - return convertObject(value, ignore); - } - - // support class - const name = value.name || 'anonymous'; - if (is.class(value)) { - return ``; - } - - // support generator function - if (is.function(value)) { - if (is.generatorFunction(value)) return ``; - if (is.asyncFunction(value)) return ``; - return ``; - } - - const typeName = value.constructor.name; - if (typeName) { - if (is.buffer(value) || is.string(value)) return `<${typeName} len: ${value.length}>`; - return `<${typeName}>`; - } - - /* istanbul ignore next */ - return util.format(value); -} - -function safeParseURL(url) { - try { - return new URL(url); - } catch (err) { - return null; - } -} diff --git a/src/lib/core/utils.ts b/src/lib/core/utils.ts new file mode 100644 index 0000000000..c4556c4142 --- /dev/null +++ b/src/lib/core/utils.ts @@ -0,0 +1,77 @@ +import util from 'node:util'; +import { + isSymbol, isRegExp, isPrimitive, + isClass, isFunction, isGeneratorFunction, isAsyncFunction, +} from 'is-type-of'; + +export function convertObject(obj: any, ignore: string | RegExp | (string | RegExp)[]) { + if (!Array.isArray(ignore)) { + ignore = [ ignore ]; + } + for (const key of Object.keys(obj)) { + obj[key] = convertValue(key, obj[key], ignore); + } + return obj; +} + +function convertValue(key: string, value: any, ignore: (string | RegExp)[]) { + if (value === null || value === undefined) { + return value; + } + + let hit = false; + for (const matchKey of ignore) { + if (typeof matchKey === 'string' && matchKey === key) { + hit = true; + break; + } else if (isRegExp(matchKey) && matchKey.test(key)) { + hit = true; + break; + } + } + if (!hit) { + if (isSymbol(value) || isRegExp(value)) { + return value.toString(); + } + if (isPrimitive(value) || Array.isArray(value)) { + return value; + } + } + + // only convert recursively when it's a plain object, + // o = {} + if (Object.getPrototypeOf(value) === Object.prototype) { + return convertObject(value, ignore); + } + + // support class + const name = value.name || 'anonymous'; + if (isClass(value)) { + return ``; + } + + // support generator function + if (isFunction(value)) { + if (isGeneratorFunction(value)) return ``; + if (isAsyncFunction(value)) return ``; + return ``; + } + + const typeName = value.constructor.name; + if (typeName) { + if (Buffer.isBuffer(value) || typeof value === 'string') { + return `<${typeName} len: ${value.length}>`; + } + return `<${typeName}>`; + } + + return util.format(value); +} + +export function safeParseURL(url: string) { + try { + return new URL(url); + } catch { + return null; + } +} diff --git a/src/lib/egg.ts b/src/lib/egg.ts index d35aefb025..7b56c88b27 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -1,25 +1,29 @@ 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 { fileURLToPath } from 'node:url'; +import ms from 'ms'; 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 type { Agent } from './agent.js'; +import type { Application } from './application.js'; +import type { EggAppConfig } from './type.js'; import ContextHttpClient from './core/context_httpclient'; -import Messenger from './core/messenger'; +import { create as createMessenger, IMessenger } 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 { +import { Singleton, type SingletonCreateMethod, type SingletonOptions, } from './core/singleton.js'; -import utils from './core/utils'; +import { convertObject } from './core/utils.js'; import { BaseContextClass } from './core/base_context_class.js'; import { BaseHookClass } from './core/base_hook_class.js'; @@ -88,6 +92,10 @@ export class EggApplication extends EggCore { #httpClient?: HttpClient; #loggers?: EggLoggers; + readonly messenger: IMessenger; + agent?: Agent; + application?: Application; + /** * @class * @param {Object} options @@ -112,7 +120,7 @@ export class EggApplication extends EggCore { * @member {Messenger} * @since 1.0.0 */ - this.messenger = Messenger.create(this); + this.messenger = createMessenger(this); // trigger `serverDidReady` hook when all the app workers // and agent worker are ready @@ -126,13 +134,13 @@ export class EggApplication extends EggCore { const dumpStartTime = Date.now(); this.dumpConfig(); this.dumpTiming(); - this.coreLogger.info('[egg:core] dump config after ready, %s', ms(Date.now() - dumpStartTime)); + this.coreLogger.info('[egg] dump config after ready, %s', ms(Date.now() - dumpStartTime)); })); 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); - this.console.info('[egg:core] Loaded enabled plugin %j', this.loader.orderPlugins); + this.console.info('[egg] App root: %s', this.baseDir); + this.console.info('[egg] All *.log files save on %j', this.config.logger.dir); + this.console.info('[egg] Loaded enabled plugin %j', this.loader.orderPlugins); // Listen the error that promise had not catch, then log it in common-error this._unhandledRejectionHandler = this._unhandledRejectionHandler.bind(this); @@ -143,7 +151,7 @@ export class EggApplication extends EggCore { this.beforeClose(async () => { // single process mode will close agent before app close if (this.type === 'application' && this.options.mode === 'single') { - await this.agent.close(); + await this.agent!.close(); } for (const logger of this.loggers.values()) { @@ -383,7 +391,7 @@ export class EggApplication extends EggCore { * @private */ dumpConfigToObject() { - let ignoreList: string[]; + let ignoreList: (string | RegExp)[]; try { // support array and set ignoreList = Array.from(this.config.dump.ignore); @@ -395,7 +403,7 @@ export class EggApplication extends EggCore { plugins: this.loader.allPlugins, appInfo: this.loader.appInfo, }); - utils.convertObject(config, ignoreList); + convertObject(config, ignoreList); return { config, meta: this.loader.configMeta, @@ -423,7 +431,7 @@ export class EggApplication extends EggCore { const dumpMetaFile = path.join(rundir, `${this.type}_config_meta.json`); fs.writeFileSync(dumpMetaFile, CircularJSON.stringify(meta, null, 2)); } catch (err) { - this.coreLogger.warn(`dumpConfig error: ${err.message}`); + this.coreLogger.warn(`[egg] dumpConfig error: ${err.message}`); } } @@ -440,17 +448,22 @@ export class EggApplication extends EggCore { for (const item of items) { // ignore #0 name: Process Start 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', + this.coreLogger.warn('[egg][dumpTiming][slow-boot-action] #%d %dms, name: %s', item.index, item.duration, item.name); } } } catch (err) { - this.coreLogger.warn(`dumpTiming error: ${err.message}`); + this.coreLogger.warn(`[egg] dumpTiming error: ${err.message}`); } } get [EGG_PATH]() { - return path.join(__dirname, '..'); + if (typeof __dirname !== 'undefined') { + return __dirname; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return path.dirname(fileURLToPath(import.meta.url)); } #setupTimeoutTimer() { @@ -463,7 +476,8 @@ export class EggApplication extends EggCore { if (item.end) continue; this.coreLogger.error(`unfinished timing item: ${CircularJSON.stringify(item)}`); } - this.coreLogger.error(`check run/${this.type}_timing_${process.pid}.json for more details.`); + this.coreLogger.error('[egg][setupTimeoutTimer] check run/%s_timing_%s.json for more details.', + this.type, process.pid); this.emit('startTimeout'); this.dumpConfig(); this.dumpTiming(); @@ -471,6 +485,10 @@ export class EggApplication extends EggCore { this.ready(() => clearTimeout(startTimeoutTimer)); } + get config() { + return super.config as EggAppConfig; + } + /** * app.env delegate app.config.env * @deprecated @@ -530,7 +548,7 @@ export 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?: any) { + createAnonymousContext(req?: unknown): EggCoreContext { const request = { headers: { host: '127.0.0.1', @@ -570,7 +588,7 @@ export class EggApplication extends EggCore { * @param {Res} res - node native Response object * @return {Context} context object */ - createContext(req: IncomingMessage, res: ServerResponse) { + createContext(req: IncomingMessage, res: ServerResponse): EggCoreContext { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); diff --git a/src/lib/start.js b/src/lib/start.js deleted file mode 100644 index 478c086d13..0000000000 --- a/src/lib/start.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -const path = require('path'); - -module.exports = async (options = {}) => { - - options.baseDir = options.baseDir || process.cwd(); - options.mode = 'single'; - - // get agent from options.framework and package.egg.framework - if (!options.framework) { - try { - options.framework = require(path.join(options.baseDir, 'package.json')).egg.framework; - } catch (_) { - // ignore - } - } - let Agent; - let Application; - if (options.framework) { - Agent = require(options.framework).Agent; - Application = require(options.framework).Application; - } else { - Application = require('./application'); - Agent = require('./agent'); - } - - const agent = new Agent(Object.assign({}, options)); - await agent.ready(); - const application = new Application(Object.assign({}, options)); - application.agent = agent; - agent.application = application; - await application.ready(); - - // emit egg-ready message in agent and application - application.messenger.broadcast('egg-ready'); - - return application; -}; diff --git a/src/lib/start.ts b/src/lib/start.ts new file mode 100644 index 0000000000..6dc1262e84 --- /dev/null +++ b/src/lib/start.ts @@ -0,0 +1,51 @@ +import path from 'node:path'; +import { readJSON } from 'utility'; +import { importModule } from '@eggjs/utils'; +import { Agent } from './agent.js'; +import { Application } from './application.js'; + +export interface StartOptions { + /** specify framework that can be absolute path or npm package */ + framework?: string; + /** directory of application, default to `process.cwd()` */ + baseDir?: string; + /** ignore single process mode warning */ + ignoreWarning?: boolean; + mode?: 'single'; +} + +/** + * Start egg with single process + */ +export default async function startEgg(options: StartOptions = {}) { + options.baseDir = options.baseDir ?? process.cwd(); + options.mode = 'single'; + + // get agent from options.framework and package.egg.framework + if (!options.framework) { + try { + const pkg = await readJSON(path.join(options.baseDir, 'package.json')); + options.framework = pkg.egg.framework; + } catch (_) { + // ignore + } + } + let AgentClass = Agent; + let ApplicationClass = Application; + if (options.framework) { + const framework = await importModule(options.framework, { paths: [ options.baseDir ] }); + AgentClass = framework.Agent; + ApplicationClass = framework.Application; + } + + const agent = new AgentClass(Object.assign({}, options)); + await agent.ready(); + const application = new ApplicationClass(Object.assign({}, options)); + application.agent = agent; + agent.application = application; + await application.ready(); + + // emit egg-ready message in agent and application + application.messenger.broadcast('egg-ready'); + return application; +} From 54abba8d33d57590abe1c330b49942165695cad2 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 14:38:21 +0800 Subject: [PATCH 04/12] createLoggers --- .gitignore | 1 + package.json | 15 ++------------- src/lib/core/logger.ts | 28 ++++++++++++++++------------ src/lib/egg.ts | 8 ++++---- src/lib/type.ts | 2 +- tsconfig.json | 18 ++++++++++++++++++ 6 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 8fe303468d..cd6dbe2e05 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ site/dist .umi-production .vercel package-lock.json +.tshy* diff --git a/package.json b/package.json index cdef1ad522..deb5a43d97 100644 --- a/package.json +++ b/package.json @@ -63,14 +63,10 @@ "@types/koa-bodyparser": "^4.3.12", "@types/ms": "^0.7.34", "@types/node": "20", - "@umijs/preset-react": "^2.1.6", "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", "egg-mock": "^5.12.0", "egg-plugin-puml": "^2.4.0", @@ -78,24 +74,17 @@ "egg-view-nunjucks": "^2.3.0", "eslint": "8", "eslint-config-egg": "14", - "findlinks": "^2.2.0", "formstream": "^1.5.1", - "jsdoc": "^3.6.11", - "koa": "^2.13.4", "koa-static": "^5.0.0", - "node-libs-browser": "^2.2.1", "pedding": "^1.1.0", "prettier": "^2.7.1", - "puppeteer": "^19.11.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-router": "^5.3.4", "runscript": "^1.5.3", "sdk-base": "^4.2.1", "spy": "^1.0.0", "supertest": "^6.2.4", - "ts-node": "10", "tsd": "^0.31.1", + "tshy": "^1.18.0", + "tshy-after": "^1.1.1", "typescript": "5", "umi": "^3.5.36" }, diff --git a/src/lib/core/logger.ts b/src/lib/core/logger.ts index 745796aa95..183b9f599c 100644 --- a/src/lib/core/logger.ts +++ b/src/lib/core/logger.ts @@ -1,23 +1,27 @@ -import { EggLoggers } from 'egg-logger'; +import { EggLoggers, EggLoggersOptions } from 'egg-logger'; import { setCustomLogger } from 'onelogger'; import type { EggApplication } from '../egg.js'; -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; + const loggerOptions = { + ...app.config.logger, + type: app.type, + localStorage: app.ctxStorage, + } as EggLoggersOptions; - if (app.config.env === 'prod' && loggerConfig.level === 'DEBUG' && !loggerConfig.allowDebugAtProd) { - loggerConfig.level = 'INFO'; + // set DEBUG level into INFO on prod env + if (app.config.env === 'prod' && loggerOptions.level === 'DEBUG' && !app.config.logger.allowDebugAtProd) { + loggerOptions.level = 'INFO'; } - const loggers = new EggLoggers(app.config); + const loggers = new EggLoggers({ + logger: loggerOptions, + customLogger: app.config.customLogger, + }); // won't print to console after started, except for local and unittest app.ready(() => { - if (loggerConfig.disableConsoleAfterReady) { + if (app.config.logger.disableConsoleAfterReady) { loggers.disableConsole(); loggers.coreLogger.info('[egg:lib:core:logger] disable console log after app ready'); } @@ -28,11 +32,11 @@ export function createLoggers(app: EggApplication) { setCustomLogger(loggerName, loggers[loggerName]); } // reset global logger on beforeClose hook - app.beforeClose(() => { + app.lifecycle.registerBeforeClose(() => { for (const loggerName of Object.keys(loggers)) { setCustomLogger(loggerName, undefined); } }); - loggers.coreLogger.info('[egg:lib:core:logger] init all loggers with options: %j', loggerConfig); + loggers.coreLogger.info('[egg:lib:core:logger] init all loggers with options: %j', loggerOptions); return loggers; } diff --git a/src/lib/egg.ts b/src/lib/egg.ts index 7b56c88b27..c4322ec7a5 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -19,7 +19,7 @@ import { create as createMessenger, IMessenger } 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 { createLoggers, type EggLoggers, type EggLogger } from './core/logger.js'; import { Singleton, type SingletonCreateMethod, type SingletonOptions, } from './core/singleton.js'; @@ -548,8 +548,8 @@ export 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?: unknown): EggCoreContext { - const request = { + createAnonymousContext(req?: any): EggCoreContext { + const request: any = { headers: { host: '127.0.0.1', 'x-forwarded-for': '127.0.0.1', @@ -567,7 +567,7 @@ export 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') { diff --git a/src/lib/type.ts b/src/lib/type.ts index 3fa700921c..485f28e586 100644 --- a/src/lib/type.ts +++ b/src/lib/type.ts @@ -31,7 +31,7 @@ export interface ClientErrorResponse { export type EggEnvType = 'local' | 'unittest' | 'prod' | string; /** logger config of egg */ -interface EggLoggerConfig extends Omit { +export interface EggLoggerConfig extends Omit { /** custom config of coreLogger */ coreLogger?: Partial; /** allow debug log at prod, defaults to `false` */ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..7f39495db7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "jsx": "react", + "module": "nodenext", + "moduleResolution": "nodenext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2022" + } +} From 214b05cceb67fce5727f5b861393f164ac0e57a9 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 15:08:27 +0800 Subject: [PATCH 05/12] context_httpclient --- src/config/config.default.ts | 2 -- src/index.d.ts | 15 ----------- src/lib/core/context_httpclient.js | 26 ------------------ src/lib/core/context_httpclient.ts | 33 +++++++++++++++++++++++ src/lib/core/httpclient.ts | 15 ++++++----- src/lib/core/singleton.ts | 4 +-- src/lib/egg.ts | 43 ++++++++++++++++-------------- src/lib/type.ts | 3 +-- tsconfig.json | 2 +- 9 files changed, 69 insertions(+), 74 deletions(-) delete mode 100644 src/lib/core/context_httpclient.js create mode 100644 src/lib/core/context_httpclient.ts diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 625ef5d355..1b3c488435 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -264,7 +264,6 @@ export default (appInfo: EggAppInfo) => { * @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 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 = { @@ -282,7 +281,6 @@ export default (appInfo: EggAppInfo) => { errorLogName: 'common-error.log', coreLogger: {}, allowDebugAtProd: false, - enablePerformanceTimer: true, enableFastContextLogger: true, }; diff --git a/src/index.d.ts b/src/index.d.ts index 1dbf24df55..6ec793487a 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -245,20 +245,6 @@ declare module 'egg' { type IgnoreItem = string | RegExp | ((ctx: Context) => boolean); type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; - /** logger config of egg */ - export interface EggLoggerConfig extends RemoveSpecProp { - /** 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 RemoveSpecProp { /** @@ -371,7 +357,6 @@ declare module 'egg' { * @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: EggLoggerConfig; diff --git a/src/lib/core/context_httpclient.js b/src/lib/core/context_httpclient.js deleted file mode 100644 index a1db7d26a5..0000000000 --- a/src/lib/core/context_httpclient.js +++ /dev/null @@ -1,26 +0,0 @@ -class ContextHttpClient { - constructor(ctx) { - this.ctx = ctx; - this.app = ctx.app; - } - - /** - * http request helper base on {@link HttpClient}, it will auto save httpclient log. - * Keep the same api with {@link Application#curl}. - * - * @param {String|Object} url - request url address. - * @param {Object} [options] - options for request. - * @return {Object} see {@link Application#curl} - */ - async curl(url, options) { - options = options || {}; - options.ctx = this.ctx; - return await this.app.curl(url, options); - } - - async request(url, options) { - return await this.curl(url, options); - } -} - -module.exports = ContextHttpClient; diff --git a/src/lib/core/context_httpclient.ts b/src/lib/core/context_httpclient.ts new file mode 100644 index 0000000000..a3336e6375 --- /dev/null +++ b/src/lib/core/context_httpclient.ts @@ -0,0 +1,33 @@ +import type { EggApplicationContext, EggApplication } from '../egg.js'; +import type { + HttpClientRequestURL, HttpClientRequestOptions, +} from './httpclient.js'; + +export class ContextHttpClient { + ctx: EggApplicationContext; + app: EggApplication; + + constructor(ctx: EggApplicationContext) { + this.ctx = ctx; + this.app = ctx.app; + } + + /** + * http request helper base on {@link HttpClient}, it will auto save httpclient log. + * Keep the same api with {@link Application#curl}. + * + * @param {String|Object} url - request url address. + * @param {Object} [options] - options for request. + */ + async curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions) { + options = { + ...options, + ctx: this.ctx, + }; + return await this.app.curl(url, options); + } + + async request(url: HttpClientRequestURL, options?: HttpClientRequestOptions) { + return await this.curl(url, options); + } +} diff --git a/src/lib/core/httpclient.ts b/src/lib/core/httpclient.ts index d2f084f549..5e0a1bc431 100644 --- a/src/lib/core/httpclient.ts +++ b/src/lib/core/httpclient.ts @@ -1,13 +1,16 @@ import { EggCoreContext } from '@eggjs/core'; import { HttpClient as RawHttpClient, - RequestURL, RequestOptions, + RequestURL as HttpClientRequestURL, + RequestOptions, } from 'urllib'; import ms from 'ms'; import type { EggApplication } from '../egg.js'; -export type { HttpClientResponse } from 'urllib'; -export type HttpClientRequestURL = RequestURL; +export type { + HttpClientResponse, + RequestURL as HttpClientRequestURL, +} from 'urllib'; export interface HttpClientRequestOptions extends RequestOptions { ctx?: EggCoreContext; @@ -26,7 +29,7 @@ export class HttpClient extends RawHttpClient { this.#app = app; } - async request(url: RequestURL, options?: HttpClientRequestOptions) { + async request(url: HttpClientRequestURL, options?: HttpClientRequestOptions) { options = options ?? {}; if (options.ctx?.tracer) { options.tracer = options.ctx.tracer; @@ -36,7 +39,7 @@ export class HttpClient extends RawHttpClient { return await super.request(url, options); } - async curl(url: RequestURL, options?: HttpClientRequestOptions) { + async curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions) { return await this.request(url, options); } } @@ -44,6 +47,6 @@ export class HttpClient extends RawHttpClient { function normalizeConfig(app: EggApplication) { const config = app.config.httpclient; if (typeof config.request.timeout === 'string') { - config.request.timeout = ms(config.request.timeout); + config.request.timeout = ms(config.request.timeout as string); } } diff --git a/src/lib/core/singleton.ts b/src/lib/core/singleton.ts index 6b50cfd142..f736c272d9 100644 --- a/src/lib/core/singleton.ts +++ b/src/lib/core/singleton.ts @@ -104,7 +104,7 @@ export class Singleton { config = { ...this.options.default, ...config, - } + }; return (this.create as SingletonCreateMethod)(config, this.app, clientName); } @@ -113,7 +113,7 @@ export class Singleton { config = { ...this.options.default, ...config, - } + }; return await this.create(config, this.app, clientName); } diff --git a/src/lib/egg.ts b/src/lib/egg.ts index c4322ec7a5..2eb0ca79cd 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -8,18 +8,18 @@ import ms from 'ms'; 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 { EggContextLogger as ContextLogger, EggLoggers, EggLogger } from 'egg-logger'; import { Cookies as ContextCookies } from '@eggjs/cookies'; import CircularJSON from 'circular-json-for-egg'; import type { Agent } from './agent.js'; import type { Application } from './application.js'; import type { EggAppConfig } from './type.js'; -import ContextHttpClient from './core/context_httpclient'; -import { create as createMessenger, IMessenger } from './core/messenger'; +import { create as createMessenger, IMessenger } from './core/messenger/index.js'; +import { ContextHttpClient } from './core/context_httpclient.js'; import { HttpClient, type HttpClientRequestOptions, type HttpClientRequestURL, type HttpClientResponse, } from './core/httpclient.js'; -import { createLoggers, type EggLoggers, type EggLogger } from './core/logger.js'; +import { createLoggers } from './core/logger.js'; import { Singleton, type SingletonCreateMethod, type SingletonOptions, } from './core/singleton.js'; @@ -35,6 +35,20 @@ export interface EggApplicationOptions extends EggCoreOptions { clusterPort?: number; } +export interface EggApplicationContext extends EggCoreContext { + app: EggApplication; + /** + * Request start time + * @member {Number} Context#starttime + */ + starttime: number; + /** + * Request start timer using `performance.now()` + * @member {Number} Context#performanceStarttime + */ + performanceStarttime: number; +} + /** * Based on koa's Application * @see https://github.com/eggjs/egg-core @@ -87,7 +101,7 @@ export class EggApplication extends EggCore { */ Boot = BaseHookClass; - options: EggApplicationOptions; + declare options: EggApplicationOptions; #httpClient?: HttpClient; #loggers?: EggLoggers; @@ -588,8 +602,8 @@ export class EggApplication extends EggCore { * @param {Res} res - node native Response object * @return {Context} context object */ - createContext(req: IncomingMessage, res: ServerResponse): EggCoreContext { - const context = Object.create(this.context); + createContext(req: IncomingMessage, res: ServerResponse): EggApplicationContext { + const context = Object.create(this.context) as EggApplicationContext; const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; @@ -600,19 +614,8 @@ export class EggApplication extends EggCore { response.request = request; context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url; - - /** - * Request start time - * @member {Number} Context#starttime - */ context.starttime = Date.now(); - if (this.config.logger.enablePerformanceTimer) { - /** - * Request start timer using `performance.now()` - * @member {Number} Context#performanceStarttime - */ - context.performanceStarttime = performance.now(); - } - return context as unknown as EggCoreContext; + context.performanceStarttime = performance.now(); + return context; } } diff --git a/src/lib/type.ts b/src/lib/type.ts index 485f28e586..31a56db074 100644 --- a/src/lib/type.ts +++ b/src/lib/type.ts @@ -38,7 +38,7 @@ export interface EggLoggerConfig extends Omit { 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. */ + /** [deprecated] Defaults to `true`. */ enablePerformanceTimer?: boolean; /** using the app logger instead of EggContextLogger, defaults to `false` */ enableFastContextLogger?: boolean; @@ -121,7 +121,6 @@ export interface EggAppConfig { * @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; diff --git a/tsconfig.json b/tsconfig.json index 7f39495db7..648a72cca6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, - "strict": true, + "strict": false, "target": "es2022" } } From 235b77a001e4c70e46b077748f750d93306b7c81 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 15:14:02 +0800 Subject: [PATCH 06/12] BaseContextClass --- src/lib/core/base_context_class.ts | 13 ++++++++----- src/lib/core/base_context_logger.ts | 18 ++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/lib/core/base_context_class.ts b/src/lib/core/base_context_class.ts index 882b9a514d..7e12e75efb 100644 --- a/src/lib/core/base_context_class.ts +++ b/src/lib/core/base_context_class.ts @@ -1,7 +1,6 @@ import { BaseContextClass as EggCoreBaseContextClass } from '@eggjs/core'; -import { BaseContextLogger } from './base_context_logger'; - -const LOGGER = Symbol('BaseContextClass#logger'); +import type { EggApplicationContext } from '../egg.js'; +import { BaseContextLogger } from './base_context_logger.js'; /** * BaseContextClass is a base class that can be extended, @@ -9,10 +8,14 @@ const LOGGER = Symbol('BaseContextClass#logger'); * {@link Helper}, {@link Service} is extending it. */ export class BaseContextClass extends EggCoreBaseContextClass { + declare ctx: EggApplicationContext; protected pathName?: string; + #logger: BaseContextLogger; get logger() { - if (!this[LOGGER]) this[LOGGER] = new BaseContextLogger(this.ctx, this.pathName); - return this[LOGGER]; + if (!this.#logger) { + this.#logger = new BaseContextLogger(this.ctx, this.pathName); + } + return this.#logger; } } diff --git a/src/lib/core/base_context_logger.ts b/src/lib/core/base_context_logger.ts index b73758c722..07df7bc8b6 100644 --- a/src/lib/core/base_context_logger.ts +++ b/src/lib/core/base_context_logger.ts @@ -1,9 +1,7 @@ -import { type EggCoreContext } from '@eggjs/core'; - -const CALL = Symbol('BaseContextLogger#call'); +import type { EggApplicationContext } from '../egg.js'; export class BaseContextLogger { - readonly #ctx: EggCoreContext; + readonly #ctx: EggApplicationContext; readonly #pathName?: string; /** @@ -12,7 +10,7 @@ export class BaseContextLogger { * @param {String} pathName - class path name * @since 1.0.0 */ - constructor(ctx: EggCoreContext, pathName?: string) { + constructor(ctx: EggApplicationContext, pathName?: string) { /** * @member {Context} BaseContextLogger#ctx * @since 1.2.0 @@ -21,7 +19,7 @@ export class BaseContextLogger { this.#pathName = pathName; } - [CALL](method: string, args: any[]) { + protected _log(method: string, args: any[]) { // add `[${pathName}]` in log if (this.#pathName && typeof args[0] === 'string') { args[0] = `[${this.#pathName}] ${args[0]}`; @@ -35,7 +33,7 @@ export class BaseContextLogger { * @since 1.2.0 */ debug(...args: any[]) { - this[CALL]('debug', args); + this._log('debug', args); } /** @@ -44,7 +42,7 @@ export class BaseContextLogger { * @since 1.2.0 */ info(...args: any[]) { - this[CALL]('info', args); + this._log('info', args); } /** @@ -53,7 +51,7 @@ export class BaseContextLogger { * @since 1.2.0 */ warn(...args: any[]) { - this[CALL]('warn', args); + this._log('warn', args); } /** @@ -62,6 +60,6 @@ export class BaseContextLogger { * @since 1.2.0 */ error(...args: any[]) { - this[CALL]('error', args); + this._log('error', args); } } From 37abca9a2e7a8d250dea245a6c9cc9c70d4e2f6f Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 21:39:23 +0800 Subject: [PATCH 07/12] Agent --- src/agent.js | 11 ---- src/agent.ts | 7 +++ src/index.test-d.ts | 1 - src/{index.js => index.ts} | 24 +++++---- src/lib/agent.ts | 61 ++++++++------------- src/lib/application.ts | 6 +-- src/lib/core/base_hook_class.ts | 25 ++++----- src/lib/egg.ts | 74 ++++++++++++++++---------- src/lib/loader/EggApplicationLoader.ts | 5 ++ src/lib/loader/agent_worker_loader.js | 27 ---------- src/lib/loader/agent_worker_loader.ts | 21 ++++++++ src/lib/loader/app_worker_loader.js | 48 ----------------- src/lib/loader/app_worker_loader.ts | 42 +++++++++++++++ src/lib/loader/index.js | 5 -- src/lib/loader/index.ts | 3 ++ src/lib/start.ts | 2 +- tsconfig.json | 2 +- 17 files changed, 176 insertions(+), 188 deletions(-) delete mode 100644 src/agent.js create mode 100644 src/agent.ts delete mode 100644 src/index.test-d.ts rename src/{index.js => index.ts} (53%) create mode 100644 src/lib/loader/EggApplicationLoader.ts delete mode 100644 src/lib/loader/agent_worker_loader.js create mode 100644 src/lib/loader/agent_worker_loader.ts delete mode 100644 src/lib/loader/app_worker_loader.js create mode 100644 src/lib/loader/app_worker_loader.ts delete mode 100644 src/lib/loader/index.js create mode 100644 src/lib/loader/index.ts diff --git a/src/agent.js b/src/agent.js deleted file mode 100644 index 17fbdc32eb..0000000000 --- a/src/agent.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const BaseHookClass = require('./lib/core/base_hook_class'); - -class EggAgentHook extends BaseHookClass { - configDidLoad() { - this.agent._wrapMessenger(); - } -} - -module.exports = EggAgentHook; diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000000..cde0a497e7 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,7 @@ +import { BaseHookClass } from './lib/core/base_hook_class.js'; + +export class EggAgentHook extends BaseHookClass { + configDidLoad() { + this.agent._wrapMessenger(); + } +} diff --git a/src/index.test-d.ts b/src/index.test-d.ts deleted file mode 100644 index 8f7bcd336e..0000000000 --- a/src/index.test-d.ts +++ /dev/null @@ -1 +0,0 @@ -import '.'; diff --git a/src/index.js b/src/index.ts similarity index 53% rename from src/index.js rename to src/index.ts index 7e3269eda3..e404fd688a 100644 --- a/src/index.js +++ b/src/index.ts @@ -2,67 +2,69 @@ * @namespace Egg */ +import { BaseContextClass } from './lib/core/base_context_class.js'; + /** * Start egg application with cluster mode * @since 1.0.0 */ -exports.startCluster = require('egg-cluster').startCluster; +export { startCluster } from 'egg-cluster'; /** * Start egg application with single process mode * @since 1.0.0 */ -exports.start = require('./lib/start'); +export { startEgg as start } from './lib/start.js'; /** * @member {Application} Egg#Application * @since 1.0.0 */ -exports.Application = require('./lib/application'); +export { Application } from './lib/application.js'; /** * @member {Agent} Egg#Agent * @since 1.0.0 */ -exports.Agent = require('./lib/agent'); +export { Agent } from './lib/agent.js'; /** * @member {AppWorkerLoader} Egg#AppWorkerLoader * @since 1.0.0 */ -exports.AppWorkerLoader = require('./lib/loader').AppWorkerLoader; /** * @member {AgentWorkerLoader} Egg#AgentWorkerLoader * @since 1.0.0 */ -exports.AgentWorkerLoader = require('./lib/loader').AgentWorkerLoader; + +export { AppWorkerLoader, AgentWorkerLoader } from './lib/loader/index.js'; /** * @member {Controller} Egg#Controller * @since 1.1.0 */ -exports.Controller = require('./lib/core/base_context_class'); +export const Controller = BaseContextClass; /** * @member {Service} Egg#Service * @since 1.1.0 */ -exports.Service = require('./lib/core/base_context_class'); +export const Service = BaseContextClass; /** * @member {Subscription} Egg#Subscription * @since 1.10.0 */ -exports.Subscription = require('./lib/core/base_context_class'); +export const Subscription = BaseContextClass; /** * @member {BaseContextClass} Egg#BaseContextClass * @since 1.2.0 */ -exports.BaseContextClass = require('./lib/core/base_context_class'); +export { BaseContextClass } from './lib/core/base_context_class.js'; /** * @member {Boot} Egg#Boot */ -exports.Boot = require('./lib/core/base_hook_class'); +export { BaseHookClass as Boot } from './lib/core/base_hook_class.js'; diff --git a/src/lib/agent.ts b/src/lib/agent.ts index a5b71ae7ee..985d5717c6 100644 --- a/src/lib/agent.ts +++ b/src/lib/agent.ts @@ -1,6 +1,6 @@ -import ms from 'ms'; -import { EggApplication } from './egg.js'; -import { AgentWorkerLoader } from './loader'; +import { EggLogger } from 'egg-logger'; +import { EggApplication, EggApplicationOptions } from './egg.js'; +import { AgentWorkerLoader } from './loader/index.js'; const EGG_LOADER = Symbol.for('egg#loader'); @@ -9,40 +9,22 @@ const EGG_LOADER = Symbol.for('egg#loader'); * @augments EggApplication */ export class Agent extends EggApplication { + readonly #agentAliveHandler: NodeJS.Timeout; + /** * @class * @param {Object} options - see {@link EggApplication} */ - constructor(options = {}) { - options.type = 'agent'; - super(options); - - this.loader.load(); - - // dump config after loaded, ensure all the dynamic modifications will be recorded - const dumpStartTime = Date.now(); - this.dumpConfig(); - this.coreLogger.info( - '[egg:core] dump config after load, %s', - ms(Date.now() - dumpStartTime) - ); + constructor(options?: EggApplicationOptions) { + super({ + ...options, + type: 'agent', + }); // keep agent alive even it doesn't have any io tasks - this.agentAliveHandler = setInterval(() => {}, 24 * 60 * 60 * 1000); - - this._uncaughtExceptionHandler = this._uncaughtExceptionHandler.bind(this); - process.on('uncaughtException', this._uncaughtExceptionHandler); - } - - _uncaughtExceptionHandler(err) { - if (!(err instanceof Error)) { - err = new Error(String(err)); - } - /* istanbul ignore else */ - if (err.name === 'Error') { - err.name = 'unhandledExceptionError'; - } - this.coreLogger.error(err); + this.#agentAliveHandler = setInterval(() => { + this.coreLogger.info('[]'); + }, 24 * 60 * 60 * 1000); } get [EGG_LOADER]() { @@ -60,16 +42,16 @@ export class Agent extends EggApplication { wrapMethod(methodName, this.messenger, this.coreLogger); } - function wrapMethod(methodName, messenger, logger) { + function wrapMethod(methodName: string, messenger: any, logger: EggLogger) { const originMethod = messenger[methodName]; - messenger[methodName] = function() { - const stack = new Error().stack.split('\n').slice(1).join('\n'); + messenger[methodName] = function(...args: any[]) { + const stack = new Error().stack!.split('\n').slice(1).join('\n'); logger.warn( "agent can't call %s before server started\n%s", methodName, - stack + stack, ); - originMethod.apply(this, arguments); + originMethod.apply(this, args); }; messenger.prependOnceListener('egg-ready', () => { messenger[methodName] = originMethod; @@ -77,9 +59,8 @@ export class Agent extends EggApplication { } } - close() { - process.removeListener('uncaughtException', this._uncaughtExceptionHandler); - clearInterval(this.agentAliveHandler); - return super.close(); + async close() { + clearInterval(this.#agentAliveHandler); + await super.close(); } } diff --git a/src/lib/application.ts b/src/lib/application.ts index 6fcbbcc90d..085373a4c8 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -1,9 +1,9 @@ -import path from 'path'; -import fs from 'fs'; +import path from 'node:path'; +import fs from 'node:fs'; +import http from 'node:http'; import ms from 'ms'; import is from 'is-type-of'; import graceful from 'graceful'; -import http from 'http'; import cluster from 'cluster-client'; import onFinished from 'on-finished'; import { assign } from 'utility'; diff --git a/src/lib/core/base_hook_class.ts b/src/lib/core/base_hook_class.ts index a310e2fbe4..7c96155215 100644 --- a/src/lib/core/base_hook_class.ts +++ b/src/lib/core/base_hook_class.ts @@ -1,29 +1,30 @@ import assert from 'node:assert'; +import type { ILifecycleBoot } from '@eggjs/core'; +import type { Application, Agent } from '../../index.js'; -const INSTANCE = Symbol('BaseHookClass#instance'); +export class BaseHookClass implements ILifecycleBoot { + fullPath?: string; + #instance: Application | Agent; -export class BaseHookClass { - - - constructor(instance) { - this[INSTANCE] = instance; + constructor(instance: Application | Agent) { + this.#instance = instance; } get logger() { - return this[INSTANCE].logger; + return this.#instance.logger; } get config() { - return this[INSTANCE].config; + return this.#instance.config; } get app() { - assert(this[INSTANCE].type === 'application', 'agent boot should not use app instance'); - return this[INSTANCE]; + assert(this.#instance.type === 'application', 'agent boot should not use app instance'); + return this.#instance as Application; } get agent() { - assert(this[INSTANCE].type === 'agent', 'app boot should not use agent instance'); - return this[INSTANCE]; + assert(this.#instance.type === 'agent', 'app boot should not use agent instance'); + return this.#instance as Agent; } } diff --git a/src/lib/egg.ts b/src/lib/egg.ts index 2eb0ca79cd..f175a9170e 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -4,12 +4,15 @@ import fs from 'node:fs'; import http, { type IncomingMessage, type ServerResponse } from 'node:http'; import inspector from 'node:inspector'; import { fileURLToPath } from 'node:url'; -import ms from 'ms'; import { EggCore, type EggCoreContext, type EggCoreOptions } from '@eggjs/core'; -import cluster from 'cluster-client'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import createClusterClient, { close as closeClusterClient } from 'cluster-client'; import extend from 'extend2'; import { EggContextLogger as ContextLogger, EggLoggers, EggLogger } from 'egg-logger'; import { Cookies as ContextCookies } from '@eggjs/cookies'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import CircularJSON from 'circular-json-for-egg'; import type { Agent } from './agent.js'; import type { Application } from './application.js'; @@ -26,13 +29,14 @@ import { import { convertObject } from './core/utils.js'; import { BaseContextClass } from './core/base_context_class.js'; import { BaseHookClass } from './core/base_hook_class.js'; +import type { EggApplicationLoader } from './loader/index.js'; const EGG_PATH = Symbol.for('egg#eggPath'); -const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); -export interface EggApplicationOptions extends EggCoreOptions { +export interface EggApplicationOptions extends Omit { mode?: 'cluster' | 'single'; clusterPort?: number; + baseDir?: string; } export interface EggApplicationContext extends EggCoreContext { @@ -101,14 +105,16 @@ export class EggApplication extends EggCore { */ Boot = BaseHookClass; - declare options: EggApplicationOptions; + declare options: Required; #httpClient?: HttpClient; #loggers?: EggLoggers; + #clusterClients: any[] = []; readonly messenger: IMessenger; agent?: Agent; application?: Application; + declare loader: EggApplicationLoader; /** * @class @@ -126,9 +132,6 @@ export class EggApplication extends EggCore { ...options, }; super(options); - - this.loader.loadConfig(); - /** * messenger instance * @member {Messenger} @@ -141,14 +144,22 @@ export class EggApplication extends EggCore { this.messenger.once('egg-ready', () => { this.lifecycle.triggerServerDidReady(); }); + this.load(); + } + + protected async loadConfig() { + await this.loader.loadConfig(); + } + protected async load() { + await this.loadConfig(); // dump config after ready, ensure all the modifications during start will be recorded // make sure dumpConfig is the last ready callback this.ready(() => process.nextTick(() => { const dumpStartTime = Date.now(); this.dumpConfig(); this.dumpTiming(); - this.coreLogger.info('[egg] dump config after ready, %s', ms(Date.now() - dumpStartTime)); + this.coreLogger.info('[egg] dump config after ready, %sms', Date.now() - dumpStartTime); })); this.#setupTimeoutTimer(); @@ -160,9 +171,14 @@ export class EggApplication extends EggCore { this._unhandledRejectionHandler = this._unhandledRejectionHandler.bind(this); process.on('unhandledRejection', this._unhandledRejectionHandler); - this[CLUSTER_CLIENTS] = []; // register close function - this.beforeClose(async () => { + this.lifecycle.registerBeforeClose(async () => { + // close all cluster clients + for (const clusterClient of this.#clusterClients) { + await closeClusterClient(clusterClient); + } + this.#clusterClients = []; + // single process mode will close agent before app close if (this.type === 'application' && this.options.mode === 'single') { await this.agent!.close(); @@ -174,6 +190,8 @@ export class EggApplication extends EggCore { this.messenger.close(); process.removeListener('unhandledRejection', this._unhandledRejectionHandler); }); + + await this.loader.load(); } /** @@ -194,8 +212,8 @@ export class EggApplication extends EggCore { * - {Number} [maxWaitTime|30000] - leader startup max time, default is 30 seconds * @return {ClientWrapper} wrapper */ - cluster(clientClass, options) { - options = { + cluster(clientClass: unknown, options: object) { + const clientClassOptions = { ...this.config.clusterClient, ...options, singleMode: this.options.mode === 'single', @@ -207,8 +225,8 @@ export class EggApplication extends EggCore { // debug mode does not check heartbeat isCheckHeartbeat: this.config.env === 'prod' ? true : inspector.url() === undefined, }; - const client = cluster(clientClass, options); - this._patchClusterClient(client); + const client = createClusterClient(clientClass, clientClassOptions); + this.#patchClusterClient(client); return client; } @@ -431,8 +449,9 @@ export class EggApplication extends EggCore { dumpConfig() { const rundir = this.config.rundir; try { - /* istanbul ignore if */ - if (!fs.existsSync(rundir)) fs.mkdirSync(rundir); + if (!fs.existsSync(rundir)) { + fs.mkdirSync(rundir); + } // get dumped object const { config, meta } = this.dumpConfigToObject(); @@ -444,7 +463,7 @@ export class EggApplication extends EggCore { // dump config meta const dumpMetaFile = path.join(rundir, `${this.type}_config_meta.json`); fs.writeFileSync(dumpMetaFile, CircularJSON.stringify(meta, null, 2)); - } catch (err) { + } catch (err: any) { this.coreLogger.warn(`[egg] dumpConfig error: ${err.message}`); } } @@ -466,18 +485,18 @@ export class EggApplication extends EggCore { item.index, item.duration, item.name); } } - } catch (err) { + } catch (err: any) { this.coreLogger.warn(`[egg] dumpTiming error: ${err.message}`); } } get [EGG_PATH]() { if (typeof __dirname !== 'undefined') { - return __dirname; + return path.dirname(__dirname); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - return path.dirname(fileURLToPath(import.meta.url)); + return path.dirname(path.dirname(fileURLToPath(import.meta.url))); } #setupTimeoutTimer() { @@ -545,12 +564,11 @@ export class EggApplication extends EggCore { } } - _patchClusterClient(client) { - const create = client.create; - client.create = (...args) => { - const realClient = create.apply(client, args); - this[CLUSTER_CLIENTS].push(realClient); - this.beforeClose(() => cluster.close(realClient)); + #patchClusterClient(client: any) { + const rawCreate = client.create; + client.create = (...args: any) => { + const realClient = rawCreate.apply(client, args); + this.#clusterClients.push(realClient); return realClient; }; } @@ -613,7 +631,7 @@ export class EggApplication extends EggCore { request.response = response; response.request = request; context.onerror = context.onerror.bind(context); - context.originalUrl = request.originalUrl = req.url; + context.originalUrl = request.originalUrl = req.url as string; context.starttime = Date.now(); context.performanceStarttime = performance.now(); return context; diff --git a/src/lib/loader/EggApplicationLoader.ts b/src/lib/loader/EggApplicationLoader.ts new file mode 100644 index 0000000000..877efe3b42 --- /dev/null +++ b/src/lib/loader/EggApplicationLoader.ts @@ -0,0 +1,5 @@ +import { EggLoader } from '@eggjs/core'; + +export abstract class EggApplicationLoader extends EggLoader { + abstract load(): Promise; +} diff --git a/src/lib/loader/agent_worker_loader.js b/src/lib/loader/agent_worker_loader.js deleted file mode 100644 index abf4c3bde1..0000000000 --- a/src/lib/loader/agent_worker_loader.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const EggLoader = require('egg-core').EggLoader; - -/** - * Agent worker process loader - * @see https://github.com/eggjs/egg-loader - */ -class AgentWorkerLoader extends EggLoader { - - /** - * loadPlugin first, then loadConfig - */ - loadConfig() { - this.loadPlugin(); - super.loadConfig(); - } - - load() { - this.loadAgentExtend(); - this.loadContextExtend(); - - this.loadCustomAgent(); - } -} - -module.exports = AgentWorkerLoader; diff --git a/src/lib/loader/agent_worker_loader.ts b/src/lib/loader/agent_worker_loader.ts new file mode 100644 index 0000000000..3f6a0eae5a --- /dev/null +++ b/src/lib/loader/agent_worker_loader.ts @@ -0,0 +1,21 @@ +import { EggApplicationLoader } from './EggApplicationLoader.js'; + +/** + * Agent worker process loader + * @see https://github.com/eggjs/egg-core/blob/master/src/loader/egg_loader.ts + */ +export class AgentWorkerLoader extends EggApplicationLoader { + /** + * loadPlugin first, then loadConfig + */ + async loadConfig() { + await this.loadPlugin(); + await super.loadConfig(); + } + + async load() { + await this.loadAgentExtend(); + await this.loadContextExtend(); + await this.loadCustomAgent(); + } +} diff --git a/src/lib/loader/app_worker_loader.js b/src/lib/loader/app_worker_loader.js deleted file mode 100644 index f2de253de3..0000000000 --- a/src/lib/loader/app_worker_loader.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const EggLoader = require('egg-core').EggLoader; - -/** - * App worker process Loader, will load plugins - * @see https://github.com/eggjs/egg-loader - */ -class AppWorkerLoader extends EggLoader { - - /** - * loadPlugin first, then loadConfig - * @since 1.0.0 - */ - loadConfig() { - this.loadPlugin(); - super.loadConfig(); - } - - /** - * Load all directories in convention - * @since 1.0.0 - */ - load() { - // app > plugin > core - this.loadApplicationExtend(); - this.loadRequestExtend(); - this.loadResponseExtend(); - this.loadContextExtend(); - this.loadHelperExtend(); - - this.loadCustomLoader(); - - // app > plugin - this.loadCustomApp(); - // app > plugin - this.loadService(); - // app > plugin > core - this.loadMiddleware(); - // app - this.loadController(); - // app - this.loadRouter(); // Depend on controllers - } - -} - -module.exports = AppWorkerLoader; diff --git a/src/lib/loader/app_worker_loader.ts b/src/lib/loader/app_worker_loader.ts new file mode 100644 index 0000000000..2328c839ee --- /dev/null +++ b/src/lib/loader/app_worker_loader.ts @@ -0,0 +1,42 @@ +import { EggApplicationLoader } from './EggApplicationLoader.js'; + +/** + * App worker process Loader, will load plugins + * @see https://github.com/eggjs/egg-core/blob/master/src/loader/egg_loader.ts + */ +export class AppWorkerLoader extends EggApplicationLoader { + /** + * loadPlugin first, then loadConfig + * @since 1.0.0 + */ + async loadConfig() { + await this.loadPlugin(); + await super.loadConfig(); + } + + /** + * Load all directories in convention + * @since 1.0.0 + */ + async load() { + // app > plugin > core + await this.loadApplicationExtend(); + await this.loadRequestExtend(); + await this.loadResponseExtend(); + await this.loadContextExtend(); + await this.loadHelperExtend(); + + await this.loadCustomLoader(); + + // app > plugin + await this.loadCustomApp(); + // app > plugin + await this.loadService(); + // app > plugin > core + await this.loadMiddleware(); + // app + await this.loadController(); + // app + await this.loadRouter(); // Depend on controllers + } +} diff --git a/src/lib/loader/index.js b/src/lib/loader/index.js deleted file mode 100644 index 8c4e7c979d..0000000000 --- a/src/lib/loader/index.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -exports.EggLoader = require('egg-core').EggLoader; -exports.AppWorkerLoader = require('./app_worker_loader'); -exports.AgentWorkerLoader = require('./agent_worker_loader'); diff --git a/src/lib/loader/index.ts b/src/lib/loader/index.ts new file mode 100644 index 0000000000..3c3a934832 --- /dev/null +++ b/src/lib/loader/index.ts @@ -0,0 +1,3 @@ +export { EggApplicationLoader } from './EggApplicationLoader.js'; +export * from './app_worker_loader.js'; +export * from './agent_worker_loader.js'; diff --git a/src/lib/start.ts b/src/lib/start.ts index 6dc1262e84..6d07d58f77 100644 --- a/src/lib/start.ts +++ b/src/lib/start.ts @@ -17,7 +17,7 @@ export interface StartOptions { /** * Start egg with single process */ -export default async function startEgg(options: StartOptions = {}) { +export async function startEgg(options: StartOptions = {}) { options.baseDir = options.baseDir ?? process.cwd(); options.mode = 'single'; diff --git a/tsconfig.json b/tsconfig.json index 648a72cca6..7f39495db7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, - "strict": false, + "strict": true, "target": "es2022" } } From 4225a160bd97a149805e2e0e313a85c5722d82b7 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 23:25:12 +0800 Subject: [PATCH 08/12] Application --- .gitignore | 1 + package.json | 8 +- src/index.ts | 2 + src/lib/agent.ts | 2 +- src/lib/application.ts | 173 +++++++----------- src/lib/core/base_context_class.ts | 2 +- src/lib/core/base_context_logger.ts | 4 +- src/lib/core/httpclient.ts | 2 +- src/lib/core/messenger/IMessenger.ts | 2 + src/lib/core/messenger/ipc.ts | 10 +- src/lib/core/messenger/local.ts | 8 +- src/lib/core/singleton.ts | 28 +-- src/lib/egg.ts | 4 +- ..._worker_loader.ts => AgentWorkerLoader.ts} | 0 ...pp_worker_loader.ts => AppWorkerLoader.ts} | 0 src/lib/loader/index.ts | 4 +- src/lib/start.ts | 8 +- 17 files changed, 120 insertions(+), 138 deletions(-) rename src/lib/loader/{agent_worker_loader.ts => AgentWorkerLoader.ts} (100%) rename src/lib/loader/{app_worker_loader.ts => AppWorkerLoader.ts} (100%) diff --git a/.gitignore b/.gitignore index cd6dbe2e05..0a7c571c03 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ site/dist .vercel package-lock.json .tshy* +dist diff --git a/package.json b/package.json index deb5a43d97..4893d04f5f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "koa-is-json": "^1.0.0", "koa-override": "^4.0.0", "ms": "^2.1.3", - "on-finished": "^2.4.1", "onelogger": "^1.0.0", "sendmessage": "^3.0.0", "urllib": "^4.0.0", @@ -129,7 +128,6 @@ } }, "exports": { - "./package.json": "./package.json", ".": { "import": { "source": "./src/index.ts", @@ -141,8 +139,10 @@ "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } - } + }, + "./package.json": "./package.json" }, "types": "./dist/commonjs/index.d.ts", - "main": "./dist/commonjs/index.js" + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/index.ts b/src/index.ts index e404fd688a..5c68571aff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ import { BaseContextClass } from './lib/core/base_context_class.js'; * Start egg application with cluster mode * @since 1.0.0 */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export { startCluster } from 'egg-cluster'; /** diff --git a/src/lib/agent.ts b/src/lib/agent.ts index 985d5717c6..42f4d86383 100644 --- a/src/lib/agent.ts +++ b/src/lib/agent.ts @@ -15,7 +15,7 @@ export class Agent extends EggApplication { * @class * @param {Object} options - see {@link EggApplication} */ - constructor(options?: EggApplicationOptions) { + constructor(options?: Omit) { super({ ...options, type: 'agent', diff --git a/src/lib/application.ts b/src/lib/application.ts index 085373a4c8..dc0770d9cd 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -1,24 +1,17 @@ import path from 'node:path'; import fs from 'node:fs'; import http from 'node:http'; -import ms from 'ms'; -import is from 'is-type-of'; +import { Socket } from 'node:net'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import graceful from 'graceful'; -import cluster from 'cluster-client'; -import onFinished from 'on-finished'; import { assign } from 'utility'; -const eggUtils = require('egg-core').utils; -import { EggApplication } from './egg.js'; -import { AppWorkerLoader } from './loader'; +import { utils as eggUtils } from '@eggjs/core'; +import { EggApplication, EggApplicationContext, EggApplicationOptions } from './egg.js'; +import { AppWorkerLoader } from './loader/index.js'; +import { BaseContextClass } from './core/base_context_class.js'; -const KEYS = Symbol('Application#keys'); -const HELPER = Symbol('Application#Helper'); -const LOCALS = Symbol('Application#locals'); -const BIND_EVENTS = Symbol('Application#bindEvents'); -const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig'); const EGG_LOADER = Symbol.for('egg#loader'); -const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); -const RESPONSE_RAW = Symbol('Application#responseRaw'); // client error => 400 Bad Request // Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror @@ -35,55 +28,60 @@ const DEFAULT_BAD_REQUEST_RESPONSE = `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`; // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 -function escapeHeaderValue(value) { +function escapeHeaderValue(value: string) { // Protect against response splitting. The regex test is there to // minimize the performance impact in the common case. return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value; } -// Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 +/** + * The Helper class which can be used as utility function. + * We support developers to extend Helper through ${baseDir}/app/extend/helper.js , + * then you can use all method on `ctx.helper` that is a instance of Helper. + */ +class HelperClass extends BaseContextClass {} + /** * Singleton instance in App Worker, extend {@link EggApplication} * @augments EggApplication */ export class Application extends EggApplication { + // will auto set after 'server' event emit + server?: http.Server; + #locals: Record = {}; + /** + * reference to {@link Helper} + * @member {Helper} Application#Helper + */ + Helper = HelperClass; /** * @class * @param {Object} options - see {@link EggApplication} */ - constructor(options = {}) { - options.type = 'application'; - super(options); - - // will auto set after 'server' event emit - this.server = null; - - try { - this.loader.load(); - } catch (e) { - // close gracefully - this[CLUSTER_CLIENTS].forEach(cluster.close); - throw e; - } - - // dump config after loaded, ensure all the dynamic modifications will be recorded - const dumpStartTime = Date.now(); - this.dumpConfig(); - this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime)); + constructor(options?: Omit) { + super({ + ...options, + type: 'application', + }); + } - this[WARN_CONFUSED_CONFIG](); - this[BIND_EVENTS](); + protected async load() { + await super.load(); + this.#warnConfusedConfig(); + this.#bindEvents(); } get [EGG_LOADER]() { return AppWorkerLoader; } - [RESPONSE_RAW](socket, raw) { + #responseRaw(socket: Socket, raw?: any) { /* istanbul ignore next */ if (!socket.writable) return; - if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + if (!raw) { + return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + } const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body; const headers = raw.headers || {}; @@ -107,10 +105,10 @@ export class Application extends EggApplication { socket.end(`${firstLine}\r\n${responseHeaderLines}\r\n${body.toString()}`); } - onClientError(err, socket) { + onClientError(err: any, socket: Socket) { // ignore when there is no http body, it almost like an ECONNRESET if (err.rawPacket) { - this.logger.warn('A client (%s:%d) error [%s] occurred: %s', + this.logger.warn('[egg:application] A client (%s:%d) error [%s] occurred: %s', socket.remoteAddress, socket.remotePort, err.code, @@ -136,19 +134,19 @@ export class Application extends EggApplication { // + headers: {} // + status: 400 p.then(ret => { - this[RESPONSE_RAW](socket, ret || {}); + this.#responseRaw(socket, ret || {}); }).catch(err => { this.logger.error(err); - this[RESPONSE_RAW](socket); + this.#responseRaw(socket); }); } else { // because it's a raw socket object, we should return the raw HTTP response // packet. - this[RESPONSE_RAW](socket); + this.#responseRaw(socket); } } - onServer(server) { + onServer(server: http.Server) { // expose app.server this.server = server; // set ignore code @@ -157,14 +155,14 @@ export class Application extends EggApplication { /* istanbul ignore next */ graceful({ server: [ server ], - error: (err, throwErrorCount) => { + error: (err: Error, throwErrorCount: number) => { const originMessage = err.message; if (originMessage) { // shouldjs will override error property but only getter // https://github.com/shouldjs/should.js/blob/889e22ebf19a06bc2747d24cf34b25cc00b37464/lib/assertion-error.js#L26 Object.defineProperty(err, 'message', { get() { - return originMessage + ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')'; + return `${originMessage} (uncaughtException throw ${throwErrorCount} times on pid: ${process.pid})`; }, configurable: true, enumerable: false, @@ -175,10 +173,12 @@ export class Application extends EggApplication { ignoreCode: serverGracefulIgnoreCode, }); - server.on('clientError', (err, socket) => this.onClientError(err, socket)); + server.on('clientError', (err, socket) => this.onClientError(err, socket as Socket)); // server timeout - if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout); + if (typeof this.config.serverTimeout === 'number') { + server.setTimeout(this.config.serverTimeout); + } } /** @@ -187,24 +187,11 @@ export class Application extends EggApplication { * @see Context#locals */ get locals() { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - return this[LOCALS]; - } - - set locals(val) { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - - assign(this[LOCALS], val); + return this.#locals; } - handleRequest(ctx, fnMiddleware) { - this.emit('request', ctx); - onFinished(ctx.res, () => this.emit('response', ctx)); - return super.handleRequest(ctx, fnMiddleware); + set locals(val: Record) { + assign(this.#locals, val); } /** @@ -227,11 +214,11 @@ export class Application extends EggApplication { paramNames: layer.paramNames, path: layer.path, regexp: layer.regexp.toString(), - stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'), + stack: layer.stack.map((stack: any) => stack[FULLPATH] || stack._name || stack.name || 'anonymous'), }); } fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2)); - } catch (err) { + } catch (err: any) { this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`); } } @@ -241,9 +228,11 @@ export class Application extends EggApplication { * @see Context#runInBackground * @param {Function} scope - the first args is an anonymous ctx */ - runInBackground(scope) { + runInBackground(scope: (ctx: EggApplicationContext) => void) { const ctx = this.createAnonymousContext(); - if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + if (!scope.name) { + Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); + } this.ctxStorage.run(ctx, () => { ctx.runInBackground(scope); }); @@ -255,11 +244,13 @@ export class Application extends EggApplication { * @param {Function} scope - the first args is an anonymous ctx, scope should be async function * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. */ - async runInAnonymousContextScope(scope, req) { + async runInAnonymousContextScope(scope: (ctx: EggApplicationContext) => Promise, req?: unknown) { const ctx = this.createAnonymousContext(req); - if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + if (!scope.name) { + Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); + } return await this.ctxStorage.run(ctx, async () => { - return await scope(ctx); + return await scope(ctx as EggApplicationContext); }); } @@ -268,7 +259,7 @@ export class Application extends EggApplication { * @member {String} Application#keys */ get keys() { - if (!this[KEYS]) { + if (!this._keys) { if (!this.config.keys) { if (this.config.env === 'local' || this.config.env === 'unittest') { const configPath = path.join(this.config.baseDir, 'config/config.default.js'); @@ -277,31 +268,9 @@ export class Application extends EggApplication { } throw new Error('Please set config.keys first'); } - - this[KEYS] = this.config.keys.split(',').map(s => s.trim()); - } - return this[KEYS]; - } - - set keys(_) { - // ignore - } - - /** - * reference to {@link Helper} - * @member {Helper} Application#Helper - */ - get Helper() { - if (!this[HELPER]) { - /** - * The Helper class which can be used as utility function. - * We support developers to extend Helper through ${baseDir}/app/extend/helper.js , - * then you can use all method on `ctx.helper` that is a instance of Helper. - */ - class Helper extends this.BaseContextClass {} - this[HELPER] = Helper; + this._keys = this.config.keys.split(',').map(s => s.trim()); } - return this[HELPER]; + return this._keys; } /** @@ -309,17 +278,15 @@ export class Application extends EggApplication { * * @private */ - [BIND_EVENTS]() { + #bindEvents() { // Browser Cookie Limits: http://browsercookielimits.squawky.net/ this.on('cookieLimitExceed', ({ name, value, ctx }) => { const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`); err.name = 'CookieLimitExceedError'; - err.key = name; - err.cookie = value; ctx.coreLogger.error(err); }); // expose server to support websocket - this.once('server', server => this.onServer(server)); + this.once('server', (server: http.Server) => this.onServer(server)); } /** @@ -327,11 +294,11 @@ export class Application extends EggApplication { * * @private */ - [WARN_CONFUSED_CONFIG]() { + #warnConfusedConfig() { const confusedConfigurations = this.config.confusedConfigurations; Object.keys(confusedConfigurations).forEach(key => { if (this.config[key] !== undefined) { - this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.', + this.logger.warn('[egg:application] Unexpected config key `%o` exists, Please use `%o` instead.', key, confusedConfigurations[key]); } }); diff --git a/src/lib/core/base_context_class.ts b/src/lib/core/base_context_class.ts index 7e12e75efb..14a43dc4f2 100644 --- a/src/lib/core/base_context_class.ts +++ b/src/lib/core/base_context_class.ts @@ -10,7 +10,7 @@ import { BaseContextLogger } from './base_context_logger.js'; export class BaseContextClass extends EggCoreBaseContextClass { declare ctx: EggApplicationContext; protected pathName?: string; - #logger: BaseContextLogger; + #logger?: BaseContextLogger; get logger() { if (!this.#logger) { diff --git a/src/lib/core/base_context_logger.ts b/src/lib/core/base_context_logger.ts index 07df7bc8b6..70181aaa3e 100644 --- a/src/lib/core/base_context_logger.ts +++ b/src/lib/core/base_context_logger.ts @@ -19,11 +19,13 @@ export class BaseContextLogger { this.#pathName = pathName; } - protected _log(method: string, args: any[]) { + protected _log(method: 'info' | 'warn' | 'error' | 'debug', args: any[]) { // add `[${pathName}]` in log if (this.#pathName && typeof args[0] === 'string') { args[0] = `[${this.#pathName}] ${args[0]}`; } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this.#ctx.app.logger[method](...args); } diff --git a/src/lib/core/httpclient.ts b/src/lib/core/httpclient.ts index 5e0a1bc431..f04b872b8c 100644 --- a/src/lib/core/httpclient.ts +++ b/src/lib/core/httpclient.ts @@ -46,7 +46,7 @@ export class HttpClient extends RawHttpClient { function normalizeConfig(app: EggApplication) { const config = app.config.httpclient; - if (typeof config.request.timeout === 'string') { + if (typeof config.request?.timeout === 'string') { config.request.timeout = ms(config.request.timeout as string); } } diff --git a/src/lib/core/messenger/IMessenger.ts b/src/lib/core/messenger/IMessenger.ts index 1c749a3a99..4ca184aef3 100644 --- a/src/lib/core/messenger/IMessenger.ts +++ b/src/lib/core/messenger/IMessenger.ts @@ -53,4 +53,6 @@ export interface IMessenger extends EventEmitter { send(action: string, data: unknown | undefined, to: string): IMessenger; close(): void; + + onMessage(message: any): void; } diff --git a/src/lib/core/messenger/ipc.ts b/src/lib/core/messenger/ipc.ts index 8bfb1a44a4..48ddd4337a 100644 --- a/src/lib/core/messenger/ipc.ts +++ b/src/lib/core/messenger/ipc.ts @@ -23,10 +23,10 @@ export class Messenger extends EventEmitter implements IMessenger { this.on('egg-pids', pids => { this.opids = pids; }); - this._onMessage = this._onMessage.bind(this); - process.on('message', this._onMessage); + this.onMessage = this.onMessage.bind(this); + process.on('message', this.onMessage); if (!workerThreads.isMainThread) { - workerThreads.parentPort!.on('message', this._onMessage); + workerThreads.parentPort!.on('message', this.onMessage); } } @@ -116,7 +116,7 @@ export class Messenger extends EventEmitter implements IMessenger { return this; } - _onMessage(message: any) { + onMessage(message: any) { if (typeof message?.action === 'string') { debug('[%s] got message %s with %j, receiverPid: %s', this.pid, message.action, message.data, message.receiverPid); @@ -125,7 +125,7 @@ export class Messenger extends EventEmitter implements IMessenger { } close() { - process.removeListener('message', this._onMessage); + process.removeListener('message', this.onMessage); this.removeAllListeners(); } diff --git a/src/lib/core/messenger/local.ts b/src/lib/core/messenger/local.ts index 462a81c02a..0d673787b9 100644 --- a/src/lib/core/messenger/local.ts +++ b/src/lib/core/messenger/local.ts @@ -115,20 +115,20 @@ export class Messenger extends EventEmitter implements IMessenger { } if (application && application.messenger && (to === 'application' || to === 'both')) { - application.messenger._onMessage({ action, data }); + application.messenger.onMessage({ action, data }); } if (agent && agent.messenger && (to === 'agent' || to === 'both')) { - agent.messenger._onMessage({ action, data }); + agent.messenger.onMessage({ action, data }); } if (opposite && opposite.messenger && to === 'opposite') { - opposite.messenger._onMessage({ action, data }); + opposite.messenger.onMessage({ action, data }); } }); return this; } - _onMessage(message: any) { + onMessage(message: any) { if (typeof message?.action === 'string') { debug('[%s] got message %s with %j', this.pid, message.action, message.data); this.emit(message.action, message.data); diff --git a/src/lib/core/singleton.ts b/src/lib/core/singleton.ts index f736c272d9..1c0d584792 100644 --- a/src/lib/core/singleton.ts +++ b/src/lib/core/singleton.ts @@ -22,7 +22,7 @@ export class Singleton { 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`); + assert(!(options.name in options.app), `[egg:singleton] ${options.name} is already exists in app`); this.app = options.app; this.name = options.name; this.create = options.create; @@ -36,12 +36,12 @@ export class Singleton { initSync() { const options = this.options; assert(!(options.client && options.clients), - `egg:singleton ${this.name} can not set options.client and options.clients both`); + `[egg:singleton] ${this.name} can not set options.client and options.clients both`); // alias app[name] as client, but still support createInstance method if (options.client) { const client = this.createInstance(options.client, options.name); - this.app[this.name] = client; + this.#setClientToApp(client); this.#extendDynamicMethods(client); return; } @@ -52,23 +52,23 @@ export class Singleton { const client = this.createInstance(options.clients[id], id); this.clients.set(id, client); }); - this.app[this.name] = this; + this.#setClientToApp(this); return; } // no config.clients and config.client - this.app[this.name] = this; + this.#setClientToApp(this); } async initAsync() { const options = this.options; assert(!(options.client && options.clients), - `egg:singleton ${this.name} can not set options.client and options.clients both`); + `[egg:singleton] ${this.name} can not set options.client and options.clients both`); // alias app[name] as client, but still support createInstance method if (options.client) { const client = await this.createInstanceAsync(options.client, options.name); - this.app[this.name] = client; + this.#setClientToApp(client); this.#extendDynamicMethods(client); return; } @@ -79,12 +79,16 @@ export class Singleton { return this.createInstanceAsync(options.clients[id], id) .then(client => this.clients.set(id, client)); })); - this.app[this.name] = this; + this.#setClientToApp(this); return; } // no config.clients and config.client - this.app[this.name] = this; + this.#setClientToApp(this); + } + + #setClientToApp(client: unknown) { + Reflect.set(this.app, this.name, client); } get(id: string) { @@ -131,10 +135,10 @@ export 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 un-extendable', + this.app.coreLogger.warn( + '[egg:singleton] %s dynamic create is disabled because of client is un-extendable', this.name); - this.app.logger.warn(err); + this.app.coreLogger.warn(err); } } } diff --git a/src/lib/egg.ts b/src/lib/egg.ts index f175a9170e..8cabdefd8a 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -256,7 +256,7 @@ export class EggApplication extends EggCore { env: this.config.env, }; - function delegate(res, app, keys) { + function delegate(res: any, app: any, keys: string[]) { for (const key of keys) { if (app[key]) { res[key] = app[key]; @@ -264,7 +264,7 @@ export class EggApplication extends EggCore { } } - function abbr(res, app, keys) { + function abbr(res: any, app: any, keys: string[]) { for (const key of keys) { if (app[key]) { res[key] = ``; diff --git a/src/lib/loader/agent_worker_loader.ts b/src/lib/loader/AgentWorkerLoader.ts similarity index 100% rename from src/lib/loader/agent_worker_loader.ts rename to src/lib/loader/AgentWorkerLoader.ts diff --git a/src/lib/loader/app_worker_loader.ts b/src/lib/loader/AppWorkerLoader.ts similarity index 100% rename from src/lib/loader/app_worker_loader.ts rename to src/lib/loader/AppWorkerLoader.ts diff --git a/src/lib/loader/index.ts b/src/lib/loader/index.ts index 3c3a934832..0c4b332a11 100644 --- a/src/lib/loader/index.ts +++ b/src/lib/loader/index.ts @@ -1,3 +1,3 @@ export { EggApplicationLoader } from './EggApplicationLoader.js'; -export * from './app_worker_loader.js'; -export * from './agent_worker_loader.js'; +export * from './AppWorkerLoader.js'; +export * from './AgentWorkerLoader.js'; diff --git a/src/lib/start.ts b/src/lib/start.ts index 6d07d58f77..ebdf7576aa 100644 --- a/src/lib/start.ts +++ b/src/lib/start.ts @@ -38,9 +38,13 @@ export async function startEgg(options: StartOptions = {}) { ApplicationClass = framework.Application; } - const agent = new AgentClass(Object.assign({}, options)); + const agent = new AgentClass({ + ...options, + }); await agent.ready(); - const application = new ApplicationClass(Object.assign({}, options)); + const application = new ApplicationClass({ + ...options, + }); application.agent = agent; agent.application = application; await application.ready(); From d7bf4213dca42a859b5b33b8fa1cc9f94629534a Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 23:28:57 +0800 Subject: [PATCH 09/12] fix lint --- src/index.d.ts => index.d.ts | 22 +++++++++++----------- package.json | 11 ++++------- src/config/config.local.ts | 2 +- src/config/config.unittest.ts | 2 +- src/lib/egg.ts | 2 +- 5 files changed, 18 insertions(+), 21 deletions(-) rename src/index.d.ts => index.d.ts (98%) diff --git a/src/index.d.ts b/index.d.ts similarity index 98% rename from src/index.d.ts rename to index.d.ts index 6ec793487a..11374fb62f 100644 --- a/src/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ import accepts = require('accepts'); import { AsyncLocalStorage } from 'async_hooks'; -import { EventEmitter } from 'events' +import { EventEmitter } from 'events'; import { Readable } from 'stream'; import { Socket } from 'net'; import { IncomingMessage, ServerResponse } from 'http'; @@ -65,10 +65,10 @@ declare module 'egg' { export interface EggHttpClient extends EventEmitter { request(url: HttpClientRequestURL): Promise | HttpClientResponse>; request(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; + Promise | HttpClientResponse>; curl(url: HttpClientRequestURL): Promise | HttpClientResponse>; curl(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; + Promise | HttpClientResponse>; } interface EggHttpConstructor { @@ -723,13 +723,13 @@ declare module 'egg' { /** * Get current execute ctx async local storage - * @returns {AsyncLocalStorage} localStorage - store current execute Context + * @return {AsyncLocalStorage} localStorage - store current execute Context */ get ctxStorage(): AsyncLocalStorage; /** * Get current execute ctx, maybe undefined - * @returns {Context} ctx - current execute Context + * @return {Context} ctx - current execute Context */ get currentContext(): Context; } @@ -1001,7 +1001,7 @@ declare module 'egg' { * // get other fields * console.log(stream.fields); * ``` - * @method Context#getFileStream + * @function Context#getFileStream * @param {Object} options * @return {ReadStream} stream * @since 1.0.0 @@ -1052,7 +1052,7 @@ declare module 'egg' { export interface IHelper extends PlainObject, BaseContextClass { /** * Generate URL path(without host) for route. Takes the route name and a map of named params. - * @method Helper#pathFor + * @function Helper#pathFor * @param {String} name - Router Name * @param {Object} params - Other params * @@ -1068,7 +1068,7 @@ declare module 'egg' { /** * Generate full URL(with host) for route. Takes the route name and a map of named params. - * @method Helper#urlFor + * @function Helper#urlFor * @param {String} name - Router name * @param {Object} params - Other params * @example @@ -1153,7 +1153,7 @@ declare module 'egg' { ignoreWarning?: boolean; } - export function start(options?: StartOptions): Promise + export function start(options?: StartOptions): Promise; /** * Powerful Partial, Support adding ? modifier to a mapped property in deep level @@ -1165,8 +1165,8 @@ declare module 'egg' { */ export type PowerPartial = { [U in keyof T]?: T[U] extends object - ? PowerPartial - : T[U] + ? PowerPartial + : T[U] }; // send data can be number|string|boolean|object but not Set|Map diff --git a/package.json b/package.json index 4893d04f5f..2305b92856 100644 --- a/package.json +++ b/package.json @@ -81,20 +81,17 @@ "sdk-base": "^4.2.1", "spy": "^1.0.0", "supertest": "^6.2.4", - "tsd": "^0.31.1", "tshy": "^1.18.0", "tshy-after": "^1.1.1", - "typescript": "5", - "umi": "^3.5.36" + "typescript": "5" }, "scripts": { - "lint": "eslint app config lib test *.js", - "tsd": "tsd", - "pretest": "npm run lint -- --fix && npm run tsd", + "lint": "eslint src test --ext .ts", + "pretest": "npm run lint -- --fix", "test": "egg-bin test", "test:changed": "egg-bin test --changed", "cov": "egg-bin cov --timeout 100000", - "preci": "npm run lint && npm run tsd", + "preci": "npm run lint", "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", diff --git a/src/config/config.local.ts b/src/config/config.local.ts index af29a4c62f..6ba0294b1a 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -8,4 +8,4 @@ export default () => { }, }, } satisfies Partial; -} +}; diff --git a/src/config/config.unittest.ts b/src/config/config.unittest.ts index 95eacc6ab5..3c16597396 100644 --- a/src/config/config.unittest.ts +++ b/src/config/config.unittest.ts @@ -7,4 +7,4 @@ export default () => { buffer: false, }, } satisfies Partial; -} +}; diff --git a/src/lib/egg.ts b/src/lib/egg.ts index 8cabdefd8a..012ad0fe09 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -302,7 +302,7 @@ export class EggApplication extends EggCore { * See https://github.com/node-modules/urllib#api-doc for more details. * * @param {String} url request url address. - * @param {Object} opts + * @param {Object} options * - 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`. From 9f66f322d4ebb5eb1f96a42c173b36a235f0a26a Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 7 Jul 2024 23:35:38 +0800 Subject: [PATCH 10/12] first test pass --- index.d.ts => index-old.d.ts | 0 package.json | 5 +++++ test/{index.test.js => index.test.ts} | 6 ++---- tsconfig.json | 18 +++++------------- 4 files changed, 12 insertions(+), 17 deletions(-) rename index.d.ts => index-old.d.ts (100%) rename test/{index.test.js => index.test.ts} (80%) diff --git a/index.d.ts b/index-old.d.ts similarity index 100% rename from index.d.ts rename to index-old.d.ts diff --git a/package.json b/package.json index 2305b92856..8ca4140b68 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@arethetypeswrong/cli": "^0.15.3", "@eggjs/tsconfig": "1", "@types/koa-bodyparser": "^4.3.12", + "@types/mocha": "^10.0.7", "@types/ms": "^0.7.34", "@types/node": "20", "address": "2", @@ -117,6 +118,10 @@ "require": "./dist/commonjs" } }, + "files": [ + "dist", + "src" + ], "type": "module", "tshy": { "exports": { diff --git a/test/index.test.js b/test/index.test.ts similarity index 80% rename from test/index.test.js rename to test/index.test.ts index 6b52ae6fee..b345f62899 100644 --- a/test/index.test.js +++ b/test/index.test.ts @@ -1,7 +1,5 @@ -'use strict'; - -const assert = require('assert'); -const egg = require('..'); +import { strict as assert } from 'node:assert'; +import * as egg from '../src/index.js'; describe('test/index.test.js', () => { it('should expose properties', () => { diff --git a/tsconfig.json b/tsconfig.json index 7f39495db7..ff41b73422 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,10 @@ { + "extends": "@eggjs/tsconfig", "compilerOptions": { - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "inlineSources": true, - "jsx": "react", - "module": "nodenext", - "moduleResolution": "nodenext", - "noUncheckedIndexedAccess": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, "strict": true, - "target": "es2022" + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" } } From 9f327a46ba156c7eb8be198508d01f19e0669ac2 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 8 Jul 2024 13:50:14 +0800 Subject: [PATCH 11/12] release beta --- package.json | 11 +-- src/agent.ts | 2 +- src/index.ts | 8 +- src/lib/agent.ts | 12 +-- src/lib/application.ts | 18 ++-- src/lib/core/base_context_class.ts | 4 +- src/lib/core/base_context_logger.ts | 6 +- src/lib/core/context_httpclient.ts | 8 +- src/lib/core/httpclient.ts | 8 +- src/lib/core/logger.ts | 4 +- src/lib/core/messenger/index.ts | 4 +- src/lib/core/messenger/local.ts | 6 +- src/lib/core/singleton.ts | 8 +- src/lib/egg.ts | 16 ++-- src/lib/start.ts | 5 +- src/lib/type.ts | 4 +- test/asyncSupport.test.js | 27 ------ test/asyncSupport.test.ts | 24 +++++ test/fixtures/apps/async-app/app.js | 2 +- test/index.test.ts | 4 +- test/lib/start.test.js | 78 --------------- test/lib/start.test.ts | 78 +++++++++++++++ test/utils.js | 130 ------------------------- test/utils.ts | 144 ++++++++++++++++++++++++++++ 24 files changed, 315 insertions(+), 296 deletions(-) delete mode 100644 test/asyncSupport.test.js create mode 100644 test/asyncSupport.test.ts delete mode 100644 test/lib/start.test.js create mode 100644 test/lib/start.test.ts delete mode 100644 test/utils.js create mode 100644 test/utils.ts diff --git a/package.json b/package.json index 8ca4140b68..10a13a2a16 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "node": ">= 18.19.0" }, "publishConfig": { - "tag": "next" + "tag": "beta" }, "description": "A web application framework for Node.js", "keywords": [ @@ -58,6 +58,7 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", + "@eggjs/koa": "^2.19.1", "@eggjs/tsconfig": "1", "@types/koa-bodyparser": "^4.3.12", "@types/mocha": "^10.0.7", @@ -82,13 +83,13 @@ "sdk-base": "^4.2.1", "spy": "^1.0.0", "supertest": "^6.2.4", - "tshy": "^1.18.0", - "tshy-after": "^1.1.1", + "tshy": "2", + "tshy-after": "1", "typescript": "5" }, "scripts": { "lint": "eslint src test --ext .ts", - "pretest": "npm run lint -- --fix", + "pretest": "npm run lint -- --fix && npm run prepublishOnly", "test": "egg-bin test", "test:changed": "egg-bin test --changed", "cov": "egg-bin cov --timeout 100000", @@ -132,12 +133,10 @@ "exports": { ".": { "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" } diff --git a/src/agent.ts b/src/agent.ts index cde0a497e7..0f64b51da1 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -1,6 +1,6 @@ import { BaseHookClass } from './lib/core/base_hook_class.js'; -export class EggAgentHook extends BaseHookClass { +export default class EggAgentHook extends BaseHookClass { configDidLoad() { this.agent._wrapMessenger(); } diff --git a/src/index.ts b/src/index.ts index 5c68571aff..d4ea49cf8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,12 @@ */ import { BaseContextClass } from './lib/core/base_context_class.js'; +import { startEgg } from './lib/start.js'; + +// export types +export * from './lib/egg.js'; +export * from './lib/type.js'; +export * from './lib/start.js'; /** * Start egg application with cluster mode @@ -16,7 +22,7 @@ export { startCluster } from 'egg-cluster'; * Start egg application with single process mode * @since 1.0.0 */ -export { startEgg as start } from './lib/start.js'; +export const start = startEgg; /** * @member {Application} Egg#Application diff --git a/src/lib/agent.ts b/src/lib/agent.ts index 42f4d86383..49731018fe 100644 --- a/src/lib/agent.ts +++ b/src/lib/agent.ts @@ -1,21 +1,21 @@ import { EggLogger } from 'egg-logger'; -import { EggApplication, EggApplicationOptions } from './egg.js'; +import { EggApplicationCore, EggApplicationCoreOptions } from './egg.js'; import { AgentWorkerLoader } from './loader/index.js'; const EGG_LOADER = Symbol.for('egg#loader'); /** - * Singleton instance in Agent Worker, extend {@link EggApplication} - * @augments EggApplication + * Singleton instance in Agent Worker, extend {@link EggApplicationCore} + * @augments EggApplicationCore */ -export class Agent extends EggApplication { +export class Agent extends EggApplicationCore { readonly #agentAliveHandler: NodeJS.Timeout; /** * @class - * @param {Object} options - see {@link EggApplication} + * @param {Object} options - see {@link EggApplicationCore} */ - constructor(options?: Omit) { + constructor(options?: Omit) { super({ ...options, type: 'agent', diff --git a/src/lib/application.ts b/src/lib/application.ts index dc0770d9cd..211a614517 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -7,7 +7,7 @@ import { Socket } from 'node:net'; import graceful from 'graceful'; import { assign } from 'utility'; import { utils as eggUtils } from '@eggjs/core'; -import { EggApplication, EggApplicationContext, EggApplicationOptions } from './egg.js'; +import { EggApplicationCore, EggContext, EggApplicationCoreOptions } from './egg.js'; import { AppWorkerLoader } from './loader/index.js'; import { BaseContextClass } from './core/base_context_class.js'; @@ -42,10 +42,10 @@ function escapeHeaderValue(value: string) { class HelperClass extends BaseContextClass {} /** - * Singleton instance in App Worker, extend {@link EggApplication} - * @augments EggApplication + * Singleton instance in App Worker, extend {@link EggApplicationCore} + * @augments EggApplicationCore */ -export class Application extends EggApplication { +export class Application extends EggApplicationCore { // will auto set after 'server' event emit server?: http.Server; #locals: Record = {}; @@ -57,9 +57,9 @@ export class Application extends EggApplication { /** * @class - * @param {Object} options - see {@link EggApplication} + * @param {Object} options - see {@link EggApplicationCore} */ - constructor(options?: Omit) { + constructor(options?: Omit) { super({ ...options, type: 'application', @@ -228,7 +228,7 @@ export class Application extends EggApplication { * @see Context#runInBackground * @param {Function} scope - the first args is an anonymous ctx */ - runInBackground(scope: (ctx: EggApplicationContext) => void) { + runInBackground(scope: (ctx: EggContext) => void) { const ctx = this.createAnonymousContext(); if (!scope.name) { Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); @@ -244,13 +244,13 @@ export class Application extends EggApplication { * @param {Function} scope - the first args is an anonymous ctx, scope should be async function * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. */ - async runInAnonymousContextScope(scope: (ctx: EggApplicationContext) => Promise, req?: unknown) { + async runInAnonymousContextScope(scope: (ctx: EggContext) => Promise, req?: unknown) { const ctx = this.createAnonymousContext(req); if (!scope.name) { Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); } return await this.ctxStorage.run(ctx, async () => { - return await scope(ctx as EggApplicationContext); + return await scope(ctx as EggContext); }); } diff --git a/src/lib/core/base_context_class.ts b/src/lib/core/base_context_class.ts index 14a43dc4f2..96a9e40a61 100644 --- a/src/lib/core/base_context_class.ts +++ b/src/lib/core/base_context_class.ts @@ -1,5 +1,5 @@ import { BaseContextClass as EggCoreBaseContextClass } from '@eggjs/core'; -import type { EggApplicationContext } from '../egg.js'; +import type { EggContext } from '../egg.js'; import { BaseContextLogger } from './base_context_logger.js'; /** @@ -8,7 +8,7 @@ import { BaseContextLogger } from './base_context_logger.js'; * {@link Helper}, {@link Service} is extending it. */ export class BaseContextClass extends EggCoreBaseContextClass { - declare ctx: EggApplicationContext; + declare ctx: EggContext; protected pathName?: string; #logger?: BaseContextLogger; diff --git a/src/lib/core/base_context_logger.ts b/src/lib/core/base_context_logger.ts index 70181aaa3e..eba0ac32c4 100644 --- a/src/lib/core/base_context_logger.ts +++ b/src/lib/core/base_context_logger.ts @@ -1,7 +1,7 @@ -import type { EggApplicationContext } from '../egg.js'; +import type { EggContext } from '../egg.js'; export class BaseContextLogger { - readonly #ctx: EggApplicationContext; + readonly #ctx: EggContext; readonly #pathName?: string; /** @@ -10,7 +10,7 @@ export class BaseContextLogger { * @param {String} pathName - class path name * @since 1.0.0 */ - constructor(ctx: EggApplicationContext, pathName?: string) { + constructor(ctx: EggContext, pathName?: string) { /** * @member {Context} BaseContextLogger#ctx * @since 1.2.0 diff --git a/src/lib/core/context_httpclient.ts b/src/lib/core/context_httpclient.ts index a3336e6375..9ae77d3220 100644 --- a/src/lib/core/context_httpclient.ts +++ b/src/lib/core/context_httpclient.ts @@ -1,13 +1,13 @@ -import type { EggApplicationContext, EggApplication } from '../egg.js'; +import type { EggContext, EggApplicationCore } from '../egg.js'; import type { HttpClientRequestURL, HttpClientRequestOptions, } from './httpclient.js'; export class ContextHttpClient { - ctx: EggApplicationContext; - app: EggApplication; + ctx: EggContext; + app: EggApplicationCore; - constructor(ctx: EggApplicationContext) { + constructor(ctx: EggContext) { this.ctx = ctx; this.app = ctx.app; } diff --git a/src/lib/core/httpclient.ts b/src/lib/core/httpclient.ts index f04b872b8c..9a55038de2 100644 --- a/src/lib/core/httpclient.ts +++ b/src/lib/core/httpclient.ts @@ -5,7 +5,7 @@ import { RequestOptions, } from 'urllib'; import ms from 'ms'; -import type { EggApplication } from '../egg.js'; +import type { EggApplicationCore } from '../egg.js'; export type { HttpClientResponse, @@ -18,9 +18,9 @@ export interface HttpClientRequestOptions extends RequestOptions { } export class HttpClient extends RawHttpClient { - readonly #app: EggApplication & { tracer?: unknown }; + readonly #app: EggApplicationCore & { tracer?: unknown }; - constructor(app: EggApplication) { + constructor(app: EggApplicationCore) { normalizeConfig(app); const config = app.config.httpclient; super({ @@ -44,7 +44,7 @@ export class HttpClient extends RawHttpClient { } } -function normalizeConfig(app: EggApplication) { +function normalizeConfig(app: EggApplicationCore) { const config = app.config.httpclient; if (typeof config.request?.timeout === 'string') { config.request.timeout = ms(config.request.timeout as string); diff --git a/src/lib/core/logger.ts b/src/lib/core/logger.ts index 183b9f599c..e9c4dcbd7b 100644 --- a/src/lib/core/logger.ts +++ b/src/lib/core/logger.ts @@ -1,8 +1,8 @@ import { EggLoggers, EggLoggersOptions } from 'egg-logger'; import { setCustomLogger } from 'onelogger'; -import type { EggApplication } from '../egg.js'; +import type { EggApplicationCore } from '../egg.js'; -export function createLoggers(app: EggApplication) { +export function createLoggers(app: EggApplicationCore) { const loggerOptions = { ...app.config.logger, type: app.type, diff --git a/src/lib/core/messenger/index.ts b/src/lib/core/messenger/index.ts index 9c283c5a40..df8784a83b 100644 --- a/src/lib/core/messenger/index.ts +++ b/src/lib/core/messenger/index.ts @@ -1,14 +1,14 @@ import { Messenger as LocalMessenger } from './local.js'; import { Messenger as IPCMessenger } from './ipc.js'; import type { IMessenger } from './IMessenger.js'; -import type { EggApplication } from '../../egg.js'; +import type { EggApplicationCore } from '../../egg.js'; export type { IMessenger } from './IMessenger.js'; /** * @class Messenger */ -export function create(egg: EggApplication): IMessenger { +export function create(egg: EggApplicationCore): IMessenger { return egg.options.mode === 'single' ? new LocalMessenger(egg) : new IPCMessenger(); diff --git a/src/lib/core/messenger/local.ts b/src/lib/core/messenger/local.ts index 0d673787b9..99fa991b2b 100644 --- a/src/lib/core/messenger/local.ts +++ b/src/lib/core/messenger/local.ts @@ -1,7 +1,7 @@ import { debuglog } from 'node:util'; import EventEmitter from 'node:events'; import type { IMessenger } from './IMessenger.js'; -import type { EggApplication } from '../../egg.js'; +import type { EggApplicationCore } from '../../egg.js'; const debug = debuglog('egg:lib:core:messenger:local'); @@ -10,9 +10,9 @@ const debug = debuglog('egg:lib:core:messenger:local'); */ export class Messenger extends EventEmitter implements IMessenger { readonly pid: string; - readonly egg: EggApplication; + readonly egg: EggApplicationCore; - constructor(egg: EggApplication) { + constructor(egg: EggApplicationCore) { super(); this.egg = egg; this.pid = String(process.pid); diff --git a/src/lib/core/singleton.ts b/src/lib/core/singleton.ts index 1c0d584792..6d71611185 100644 --- a/src/lib/core/singleton.ts +++ b/src/lib/core/singleton.ts @@ -1,19 +1,19 @@ import assert from 'node:assert'; import { isAsyncFunction } from 'is-type-of'; -import type { EggApplication } from '../egg.js'; +import type { EggApplicationCore } from '../egg.js'; export type SingletonCreateMethod = - (config: Record, app: EggApplication, clientName: string) => unknown | Promise; + (config: Record, app: EggApplicationCore, clientName: string) => unknown | Promise; export interface SingletonOptions { name: string; - app: EggApplication; + app: EggApplicationCore; create: SingletonCreateMethod; } export class Singleton { readonly clients = new Map(); - readonly app: EggApplication; + readonly app: EggApplicationCore; readonly create: SingletonCreateMethod; readonly name: string; readonly options: Record; diff --git a/src/lib/egg.ts b/src/lib/egg.ts index 012ad0fe09..209f762834 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -33,14 +33,14 @@ import type { EggApplicationLoader } from './loader/index.js'; const EGG_PATH = Symbol.for('egg#eggPath'); -export interface EggApplicationOptions extends Omit { +export interface EggApplicationCoreOptions extends Omit { mode?: 'cluster' | 'single'; clusterPort?: number; baseDir?: string; } -export interface EggApplicationContext extends EggCoreContext { - app: EggApplication; +export interface EggContext extends EggCoreContext { + app: EggApplicationCore; /** * Request start time * @member {Number} Context#starttime @@ -59,7 +59,7 @@ export interface EggApplicationContext extends EggCoreContext { * @see https://github.com/eggjs/koa/blob/master/src/application.ts * @augments EggCore */ -export class EggApplication extends EggCore { +export class EggApplicationCore extends EggCore { // export context base classes, let framework can impl sub class and over context extend easily. ContextCookies = ContextCookies; ContextLogger = ContextLogger; @@ -105,7 +105,7 @@ export class EggApplication extends EggCore { */ Boot = BaseHookClass; - declare options: Required; + declare options: Required; #httpClient?: HttpClient; #loggers?: EggLoggers; @@ -124,7 +124,7 @@ export 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?: EggApplicationOptions) { + constructor(options?: EggApplicationCoreOptions) { options = { mode: 'cluster', type: 'application', @@ -620,8 +620,8 @@ export class EggApplication extends EggCore { * @param {Res} res - node native Response object * @return {Context} context object */ - createContext(req: IncomingMessage, res: ServerResponse): EggApplicationContext { - const context = Object.create(this.context) as EggApplicationContext; + createContext(req: IncomingMessage, res: ServerResponse): EggContext { + const context = Object.create(this.context) as EggContext; const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; diff --git a/src/lib/start.ts b/src/lib/start.ts index ebdf7576aa..9a94a10573 100644 --- a/src/lib/start.ts +++ b/src/lib/start.ts @@ -4,7 +4,7 @@ import { importModule } from '@eggjs/utils'; import { Agent } from './agent.js'; import { Application } from './application.js'; -export interface StartOptions { +export interface StartEggOptions { /** specify framework that can be absolute path or npm package */ framework?: string; /** directory of application, default to `process.cwd()` */ @@ -12,12 +12,13 @@ export interface StartOptions { /** ignore single process mode warning */ ignoreWarning?: boolean; mode?: 'single'; + env?: string; } /** * Start egg with single process */ -export async function startEgg(options: StartOptions = {}) { +export async function startEgg(options: StartEggOptions = {}) { options.baseDir = options.baseDir ?? process.cwd(); options.mode = 'single'; diff --git a/src/lib/type.ts b/src/lib/type.ts index 31a56db074..603591fef8 100644 --- a/src/lib/type.ts +++ b/src/lib/type.ts @@ -10,7 +10,7 @@ import type { FileLoaderOptions, } from '@eggjs/core'; import type { - EggApplication, + EggApplicationCore, } from './egg.js'; import type { MetaMiddlewareOptions } from '../app/middleware/meta.js'; import type { NotFoundMiddlewareOptions } from '../app/middleware/notfound.js'; @@ -301,7 +301,7 @@ export interface EggAppConfig { watcher: Record; - onClientError?(err: Error, socket: Socket, app: EggApplication): ClientErrorResponse | Promise; + onClientError?(err: Error, socket: Socket, app: EggApplicationCore): ClientErrorResponse | Promise; /** * server timeout in milliseconds, default to 0 (no timeout). diff --git a/test/asyncSupport.test.js b/test/asyncSupport.test.js deleted file mode 100644 index d1b1e5d6c4..0000000000 --- a/test/asyncSupport.test.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const mm = require('egg-mock'); -const utils = require('./utils'); - -describe('test/asyncSupport.test.js', () => { - afterEach(mm.restore); - let app; - before(async () => { - app = utils.app('apps/async-app'); - await app.ready(); - assert(app.beforeStartExectuted); - assert(app.scheduleExecuted); - }); - after(async () => { - await app.close(); - assert(app.beforeCloseExecuted); - }); - - it('middleware, controller and service should support async functions', async () => { - await app.httpRequest() - .get('/api') - .expect(200) - .expect([ 'service', 'controller', 'router', 'middleware' ]); - }); -}); diff --git a/test/asyncSupport.test.ts b/test/asyncSupport.test.ts new file mode 100644 index 0000000000..6eacb010bf --- /dev/null +++ b/test/asyncSupport.test.ts @@ -0,0 +1,24 @@ +import { strict as assert } from 'node:assert'; +import * as utils from './utils.js'; + +describe('test/asyncSupport.test.ts', () => { + afterEach(utils.restore); + let app: utils.MockApplication; + before(async () => { + app = utils.app('apps/async-app'); + await app.ready(); + assert.equal(Reflect.get(app, 'beforeStartExecuted'), true); + assert.equal(Reflect.get(app, 'scheduleExecuted'), true); + }); + after(async () => { + await app.close(); + assert.equal(Reflect.get(app, 'beforeCloseExecuted'), true); + }); + + it('middleware, controller and service should support async functions', async () => { + await app.httpRequest() + .get('/api') + .expect(200) + .expect([ 'service', 'controller', 'router', 'middleware' ]); + }); +}); diff --git a/test/fixtures/apps/async-app/app.js b/test/fixtures/apps/async-app/app.js index 83f0c700ab..05f6588232 100644 --- a/test/fixtures/apps/async-app/app.js +++ b/test/fixtures/apps/async-app/app.js @@ -4,7 +4,7 @@ module.exports = app => { app.beforeStart(async () => { await Promise.resolve(); await app.runSchedule('async'); - app.beforeStartExectuted = true; + app.beforeStartExecuted = true; }); app.beforeClose(async () => { diff --git a/test/index.test.ts b/test/index.test.ts index b345f62899..3a9fab2067 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import * as egg from '../src/index.js'; -describe('test/index.test.js', () => { +describe('test/index.test.ts', () => { it('should expose properties', () => { assert.deepEqual(Object.keys(egg).sort(), [ 'Agent', @@ -11,10 +11,12 @@ describe('test/index.test.js', () => { 'BaseContextClass', 'Boot', 'Controller', + 'EggApplicationCore', 'Service', 'Subscription', 'start', 'startCluster', + 'startEgg', ]); }); }); diff --git a/test/lib/start.test.js b/test/lib/start.test.js deleted file mode 100644 index fc475ec546..0000000000 --- a/test/lib/start.test.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -const utils = require('../utils'); -const assert = require('assert'); -const path = require('path'); - -let app; - -describe('test/lib/start.test.js', () => { - afterEach(() => app.close()); - - describe('start', () => { - it('should dump config and plugins', async () => { - app = await utils.singleProcessApp('apps/demo'); - const baseDir = utils.getFilepath('apps/demo'); - let json = require(path.join(baseDir, 'run/agent_config.json')); - assert(/\d+\.\d+\.\d+/.test(json.plugins.onerror.version)); - assert(json.config.name === 'demo'); - assert(json.config.tips === 'hello egg'); - json = require(path.join(baseDir, 'run/application_config.json')); - checkApp(json); - - const dumpped = app.dumpConfigToObject(); - checkApp(dumpped.config); - - function checkApp(json) { - assert(/\d+\.\d+\.\d+/.test(json.plugins.onerror.version)); - assert(json.config.name === 'demo'); - // should dump dynamic config - assert(json.config.tips === 'hello egg started'); - } - }); - - it('should request work', async () => { - app = await utils.singleProcessApp('apps/demo'); - await app.httpRequest().get('/protocol') - .expect(200) - .expect('http'); - - await app.httpRequest().get('/class-controller') - .expect(200) - .expect('this is bar!'); - }); - - it('should env work', async () => { - app = await utils.singleProcessApp('apps/demo', { env: 'prod' }); - assert(app.config.env === 'prod'); - }); - }); - - describe('custom framework work', () => { - it('should work with options.framework', async () => { - app = await utils.singleProcessApp('apps/demo', { framework: path.join(__dirname, '../fixtures/custom-egg') }); - assert(app.customEgg); - - await app.httpRequest().get('/protocol') - .expect(200) - .expect('http'); - - await app.httpRequest().get('/class-controller') - .expect(200) - .expect('this is bar!'); - }); - - it('should work with package.egg.framework', async () => { - app = await utils.singleProcessApp('apps/custom-framework-demo'); - assert(app.customEgg); - - await app.httpRequest().get('/protocol') - .expect(200) - .expect('http'); - - await app.httpRequest().get('/class-controller') - .expect(200) - .expect('this is bar!'); - }); - }); -}); diff --git a/test/lib/start.test.ts b/test/lib/start.test.ts new file mode 100644 index 0000000000..3fdfbca734 --- /dev/null +++ b/test/lib/start.test.ts @@ -0,0 +1,78 @@ +// 'use strict'; + +// import utils from '../utils'; +// import assert from 'assert'; +// import path from 'path'; + +// let app; + +// describe('test/lib/start.test.js', () => { +// afterEach(() => app.close()); + +// describe('start', () => { +// it('should dump config and plugins', async () => { +// app = await utils.singleProcessApp('apps/demo'); +// const baseDir = utils.getFilepath('apps/demo'); +// let json = require(path.join(baseDir, 'run/agent_config.json')); +// assert(/\d+\.\d+\.\d+/.test(json.plugins.onerror.version)); +// assert(json.config.name === 'demo'); +// assert(json.config.tips === 'hello egg'); +// json = require(path.join(baseDir, 'run/application_config.json')); +// checkApp(json); + +// const dumpped = app.dumpConfigToObject(); +// checkApp(dumpped.config); + +// function checkApp(json) { +// assert(/\d+\.\d+\.\d+/.test(json.plugins.onerror.version)); +// assert(json.config.name === 'demo'); +// // should dump dynamic config +// assert(json.config.tips === 'hello egg started'); +// } +// }); + +// it('should request work', async () => { +// app = await utils.singleProcessApp('apps/demo'); +// await app.httpRequest().get('/protocol') +// .expect(200) +// .expect('http'); + +// await app.httpRequest().get('/class-controller') +// .expect(200) +// .expect('this is bar!'); +// }); + +// it('should env work', async () => { +// app = await utils.singleProcessApp('apps/demo', { env: 'prod' }); +// assert(app.config.env === 'prod'); +// }); +// }); + +// describe('custom framework work', () => { +// it('should work with options.framework', async () => { +// app = await utils.singleProcessApp('apps/demo', { framework: path.join(__dirname, '../fixtures/custom-egg') }); +// assert(app.customEgg); + +// await app.httpRequest().get('/protocol') +// .expect(200) +// .expect('http'); + +// await app.httpRequest().get('/class-controller') +// .expect(200) +// .expect('this is bar!'); +// }); + +// it('should work with package.egg.framework', async () => { +// app = await utils.singleProcessApp('apps/custom-framework-demo'); +// assert(app.customEgg); + +// await app.httpRequest().get('/protocol') +// .expect(200) +// .expect('http'); + +// await app.httpRequest().get('/class-controller') +// .expect(200) +// .expect('this is bar!'); +// }); +// }); +// }); diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index c124c51973..0000000000 --- a/test/utils.js +++ /dev/null @@ -1,130 +0,0 @@ -const { readFileSync } = require('fs'); -const { rm } = require('fs/promises'); -const path = require('path'); -const mm = require('egg-mock'); -const Koa = require('koa'); -const http = require('http'); -const request = require('supertest'); -const egg = require('..'); - -const fixtures = path.join(__dirname, 'fixtures'); -const eggPath = path.join(__dirname, '..'); - -exports.rimraf = async target => { - await rm(target, { force: true, recursive: true }); -}; - -exports.sleep = ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -}; - -exports.app = (name, options) => { - options = formatOptions(name, options); - const app = mm.app(options); - return app; -}; - -/** - * start app with single process mode - * - * @param {String} baseDir - base dir. - * @param {Object} [options] - optional - * @return {App} app - Application object. - */ -exports.singleProcessApp = async (baseDir, options = {}) => { - if (!baseDir.startsWith('/')) baseDir = path.join(__dirname, 'fixtures', baseDir); - options.env = options.env || 'unittest'; - options.baseDir = baseDir; - const app = await egg.start(options); - app.httpRequest = () => request(app.callback()); - return app; -}; - -/** - * start app with cluster mode - * - * @param {String} name - cluster name. - * @param {Object} [options] - optional - * @return {App} app - Application object. - */ -exports.cluster = (name, options) => { - options = formatOptions(name, options); - return mm.cluster(options); -}; - -let localServer; - -exports.startLocalServer = () => { - return new Promise((resolve, reject) => { - if (localServer) { - return resolve('http://127.0.0.1:' + localServer.address().port); - } - let retry = false; - - const app = new Koa(); - app.use(async ctx => { - if (ctx.path === '/get_headers') { - ctx.body = ctx.request.headers; - return; - } - - if (ctx.path === '/timeout') { - await exports.sleep(10000); - ctx.body = `${ctx.method} ${ctx.path}`; - return; - } - - if (ctx.path === '/error') { - ctx.status = 500; - ctx.body = 'this is an error'; - return; - } - - if (ctx.path === '/retry') { - if (!retry) { - retry = true; - ctx.status = 500; - } else { - ctx.set('x-retry', '1'); - ctx.body = 'retry suc'; - retry = false; - } - return; - } - - ctx.body = `${ctx.method} ${ctx.path}`; - }); - localServer = http.createServer(app.callback()); - - localServer.listen(0, err => { - if (err) return reject(err); - return resolve('http://127.0.0.1:' + localServer.address().port); - }); - }); -}; -process.once('exit', () => localServer && localServer.close()); - -exports.getFilepath = name => { - return path.join(fixtures, name); -}; - -exports.getJSON = name => { - return JSON.parse(readFileSync(exports.getFilepath(name))); -}; - -function formatOptions(name, options) { - let baseDir; - if (typeof name === 'string') { - baseDir = name; - } else { - // name is options - options = name; - } - return Object.assign({}, { - baseDir, - customEgg: eggPath, - cache: false, - }, options); -} diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000000..f210f7570b --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,144 @@ +import { readFileSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; +import path from 'node:path'; +import http from 'node:http'; +import { fileURLToPath } from 'node:url'; +import { AddressInfo } from 'node:net'; +import mm, { MockOption, MockApplication } from 'egg-mock'; +import Koa from '@eggjs/koa'; +import request from 'supertest'; +import { startEgg, StartEggOptions, Application } from '../src/index.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixtures = path.join(__dirname, 'fixtures'); +const eggPath = path.join(__dirname, '..'); + +export async function rimraf(target: string) { + await rm(target, { force: true, recursive: true }); +} + +export { MockOption, MockApplication } from 'egg-mock'; +export const restore = () => mm.default.restore(); + +export function app(name: string | MockOption, options?: MockOption) { + options = formatOptions(name, options); + const app = mm.default.app(options); + return app; +} + +/** + * start app with cluster mode + * + * @param {String} name - cluster name. + * @param {Object} [options] - optional + * @return {App} app - Application object. + */ +export function cluster(name: string | MockOption, options: MockOption): MockApplication { + options = formatOptions(name, options); + return mm.default.cluster(options); +} + +/** + * start app with single process mode + * + * @param {String} baseDir - base dir. + * @param {Object} [options] - optional + * @return {App} app - Application object. + */ +export async function singleProcessApp(baseDir: string, options: StartEggOptions = {}): Promise { + if (!baseDir.startsWith('/')) { + baseDir = path.join(__dirname, 'fixtures', baseDir); + } + options.env = options.env || 'unittest'; + options.baseDir = baseDir; + const app = await startEgg(options); + Reflect.set(app, 'httpRequest', () => request(app.callback())); + return app; +} + +let localServer: http.Server | undefined; +process.once('beforeExit', () => { + localServer && localServer.close(); + localServer = undefined; +}); +process.once('exit', () => { + localServer && localServer.close(); + localServer = undefined; +}); + +export function startLocalServer() { + return new Promise(resolve => { + if (localServer) { + const address = localServer.address() as AddressInfo; + return resolve(`http://127.0.0.1:${address.port}`); + } + let retry = false; + + const app = new Koa(); + app.use(async ctx => { + if (ctx.path === '/get_headers') { + ctx.body = ctx.request.headers; + return; + } + + if (ctx.path === '/timeout') { + await exports.sleep(10000); + ctx.body = `${ctx.method} ${ctx.path}`; + return; + } + + if (ctx.path === '/error') { + ctx.status = 500; + ctx.body = 'this is an error'; + return; + } + + if (ctx.path === '/retry') { + if (!retry) { + retry = true; + ctx.status = 500; + } else { + ctx.set('x-retry', '1'); + ctx.body = 'retry suc'; + retry = false; + } + return; + } + + ctx.body = `${ctx.method} ${ctx.path}`; + }); + localServer = http.createServer(app.callback()); + localServer.listen(0, () => { + const address = localServer!.address() as AddressInfo; + return resolve(`http://127.0.0.1:${address.port}`); + }); + }); +} + +export function getFilepath(name: string) { + return path.join(fixtures, name); +} + +export function getJSON(name: string) { + return JSON.parse(readFileSync(getFilepath(name), 'utf-8')); +} + +function formatOptions(name: string | MockOption, options?: MockOption) { + let baseDir; + if (typeof name === 'string') { + baseDir = name; + } else { + // name is options + options = name; + baseDir = options.baseDir!; + } + if (!baseDir.startsWith('/')) { + baseDir = path.join(__dirname, 'fixtures', baseDir); + } + return { + baseDir, + customEgg: eggPath, + cache: false, + ...options, + }; +} From 49f748b9cc70290997470af1df8b341cdb82cee7 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Wed, 9 Oct 2024 20:36:07 +0800 Subject: [PATCH 12/12] test sign --- README.zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.zh-CN.md b/README.zh-CN.md index 493aafdb0e..3bd09c5750 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -46,7 +46,7 @@ open http://localhost:7001 ## 贡献代码 -请告知我们可以为你做些什么,不过在此之前,请检查一下是否有[已经存在的Bug或者意见](https://github.com/eggjs/egg/issues)。 +请告知我们可以为你做些什么,不过在此之前,请检查一下是否有[已经存在的 Bug 或者意见](https://github.com/eggjs/egg/issues)。 如果你是一个代码贡献者,请参考[代码贡献规范](CONTRIBUTING.md)。