From a09b1cfcf47cae8b577a8136c927b42d989c58a5 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Mon, 30 Dec 2024 09:35:19 +0800 Subject: [PATCH] feat: refactor with typescript to support cjs ane esm both (#5328) 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 --- .eslintrc | 5 +- .github/PULL_REQUEST_TEMPLATE.md | 29 - .github/dependabot.yml | 7 - .github/workflows/codeql-analysis.yml | 68 - .github/workflows/gh-pages.yml | 2 +- .github/workflows/nodejs.yml | 10 +- .github/workflows/release.yml | 3 +- .gitignore | 4 +- README.md | 5 +- README.zh-CN.md | 12 +- agent.js | 11 - app/extend/context.js | 285 ---- app/extend/response.js | 101 -- 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 - .../app/controller/home.js | 3 + example/helloworld-commonjs/app/router.js | 3 + .../config/config.default.js | 1 + example/helloworld-commonjs/config/plugin.js | 5 + example/helloworld-commonjs/index.js | 19 + example/helloworld-commonjs/package.json | 4 + example/helloworld-typescript/app.ts | 34 + .../app/controller/home.ts | 7 + .../app/middleware/hello.ts | 13 + example/helloworld-typescript/app/router.ts | 5 + .../config/config.default.ts | 7 + example/helloworld-typescript/package.json | 10 + example/helloworld-typescript/tsconfig.json | 10 + index-old.d.ts | 1268 ++++++++++++++++ index.d.ts | 1283 ----------------- index.js | 68 - index.test-d.ts | 1 - lib/agent.js | 95 -- lib/core/base_context_class.js | 20 - lib/core/base_context_logger.js | 64 - lib/core/base_hook_class.js | 31 - lib/core/context_httpclient.js | 26 - lib/core/dnscache_httpclient.js | 93 -- lib/core/httpclient.js | 108 -- lib/core/httpclient_next.js | 37 - lib/core/logger.js | 35 - lib/core/messenger/index.js | 14 - lib/core/messenger/ipc.js | 141 -- lib/core/utils.js | 73 - lib/loader/agent_worker_loader.js | 27 - lib/loader/app_worker_loader.js | 48 - lib/loader/index.js | 5 - lib/start.js | 39 - package.json | 187 ++- scripts/commits.sh | 9 - site/docs/advanced/framework.md | 62 +- site/docs/advanced/framework.zh-CN.md | 25 +- site/docs/basics/plugin.zh-CN.md | 8 +- site/docs/basics/schedule.md | 2 +- site/docs/basics/schedule.zh-CN.md | 4 +- site/docs/core/unittest.md | 2 +- site/docs/core/unittest.zh-CN.md | 2 +- src/agent.ts | 7 + src/app/extend/context.ts | 302 ++++ src/app/extend/context.types.ts | 21 + .../helper.js => src/app/extend/helper.ts | 27 +- .../request.js => src/app/extend/request.ts | 160 +- src/app/extend/response.ts | 36 + src/app/middleware/body_parser.ts | 3 + .../meta.js => src/app/middleware/meta.ts | 15 +- .../app/middleware/notfound.ts | 11 +- src/app/middleware/override_method.ts | 3 + src/app/middleware/site_file.ts | 68 + .../config/config.default.ts | 66 +- 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 | 8 +- src/index.ts | 80 + src/lib/agent.ts | 66 + lib/application.js => src/lib/application.ts | 200 +-- src/lib/core/base_context_class.ts | 21 + src/lib/core/base_context_logger.ts | 67 + src/lib/core/base_hook_class.ts | 30 + src/lib/core/context_httpclient.ts | 33 + src/lib/core/httpclient.ts | 51 + src/lib/core/logger.ts | 42 + src/lib/core/messenger/IMessenger.ts | 58 + src/lib/core/messenger/index.ts | 15 + src/lib/core/messenger/ipc.ts | 148 ++ .../lib/core/messenger/local.ts | 64 +- .../singleton.js => src/lib/core/singleton.ts | 89 +- src/lib/core/utils.ts | 77 + lib/egg.js => src/lib/egg.ts | 498 ++++--- src/lib/egg.types.ts | 11 + src/lib/loader/AgentWorkerLoader.ts | 21 + src/lib/loader/AppWorkerLoader.ts | 42 + src/lib/loader/EggApplicationLoader.ts | 5 + src/lib/loader/index.ts | 3 + src/lib/start.ts | 56 + src/lib/type.ts | 329 +++++ src/lib/utils.ts | 16 + src/urllib.ts | 1 + test/agent.test.js | 22 - test/agent.test.ts | 19 + .../extend/{agent.test.js => agent.test.ts} | 32 +- ...pplication.test.js => application.test.ts} | 98 +- ...xt.jsonp.test.js => context.jsonp.test.ts} | 12 +- .../{context.test.js => context.test.ts} | 169 +-- .../extend/{helper.test.js => helper.test.ts} | 10 +- .../{request.test.js => request.test.ts} | 72 +- test/app/extend/response.test.js | 87 -- test/app/extend/response.test.ts | 115 ++ ...ody_parser.test.js => body_parser.test.ts} | 46 +- .../middleware/{meta.test.js => meta.test.ts} | 35 +- .../{notfound.test.js => notfound.test.ts} | 18 +- ...method.test.js => override_method.test.ts} | 8 +- .../{site_file.test.js => site_file.test.ts} | 41 +- test/asyncSupport.test.js | 27 - test/asyncSupport.test.ts | 27 + test/bench/server.js | 2 +- .../apps/agent-app-sync/app/router.js | 2 +- test/fixtures/apps/agent-app/app/router.js | 28 +- .../apps/agent-client-app/app/router.js | 16 +- test/fixtures/apps/agent-throw/app/router.js | 4 +- .../aliyun-egg-app/app/controller/home.js | 2 +- .../apps/app-die-ignore-code/app/router.js | 2 +- test/fixtures/apps/app-die/app/router.js | 4 +- .../apps/app-locals-getter/app/router.js | 2 +- .../apps/app-router/app/controller/home.js | 2 +- .../apps/app-router/config/config.default.js | 2 - .../fixtures/apps/app-router/config/plugin.js | 5 + .../app-server-customized-client-error/app.js | 2 - .../app/router.js | 4 +- .../config/config.default.js | 4 +- .../apps/app-server-timeout/app/router.js | 4 +- .../app-server-with-hostname/app/router.js | 2 +- test/fixtures/apps/app-server/app/router.js | 2 +- test/fixtures/apps/app-throw/app/router.js | 10 +- .../apps/app-ts-type-check/xiandan.d.ts | 36 +- .../apps/app-ts-type-check/yadan.d.ts | 36 +- test/fixtures/apps/async-app/app.js | 7 +- test/fixtures/apps/async-app/app/router.js | 2 - .../apps/async-app/app/schedule/async.js | 2 - .../apps/body_parser_testapp/app/router.js | 8 +- .../body_parser_testapp_disable/app/router.js | 8 +- .../body_parser_testapp_ignore/app/router.js | 8 +- .../body_parser_testapp_match/app/router.js | 8 +- test/fixtures/apps/cluster_mod_app/agent.js | 8 +- test/fixtures/apps/cluster_mod_app/app.js | 8 +- .../cluster_mod_app/app/controller/home.js | 8 +- .../apps/csrf-disable/app/controller/api.js | 2 +- .../apps/csrf-enable/app/controller/api.js | 2 +- .../apps/csrf-ignore/app/controller/api.js | 2 +- .../app/controller/home.js | 4 +- .../app/extend/context.js | 2 - .../apps/custom-env-app/app/router.js | 2 +- .../app/controller/hello.js | 2 +- .../app/controller/home.js | 2 +- .../app/controller/ip.js | 4 +- .../app/controller/logger.js | 2 +- .../apps/custom-framework-demo/app/router.js | 4 +- test/fixtures/apps/demo/app.js | 5 +- test/fixtures/apps/demo/app/controller/foo.js | 2 +- .../apps/demo/app/controller/hello.js | 2 +- .../fixtures/apps/demo/app/controller/home.js | 2 +- test/fixtures/apps/demo/app/controller/ip.js | 4 +- .../apps/demo/app/controller/logger.js | 4 +- test/fixtures/apps/demo/app/controller/obj.js | 10 +- .../fixtures/apps/demo/app/controller/obj2.js | 8 +- test/fixtures/apps/demo/app/router.js | 4 +- test/fixtures/apps/development/app/router.js | 4 +- .../app/controller/home.js | 4 +- test/fixtures/apps/dumpconfig-circular/app.js | 6 +- test/fixtures/apps/dumpconfig/app.js | 6 +- .../encrypt-cookies/app/controller/home.js | 2 +- .../favicon-buffer/app/controller/home.js | 3 + .../apps/favicon-buffer/app/router.js | 3 + .../favicon-buffer/config/config.default.js | 5 + .../fixtures/apps/favicon-buffer/package.json | 3 + .../apps/favicon/app/controller/home.js | 4 +- test/fixtures/apps/get-logger/app/router.js | 6 +- .../apps/helper/app/controller/home.js | 2 +- test/fixtures/apps/helper/app/router.js | 16 +- .../fixtures/apps/i18n/app/controller/home.js | 4 +- .../apps/i18n/app/controller/message.js | 4 +- .../apps/koa-session/app/controller/clear.js | 2 +- .../apps/koa-session/app/controller/home.js | 2 +- .../fixtures/apps/loader-plugin/app/router.js | 6 +- .../apps/loader-plugin/app/service/foo2.js | 2 +- .../loader-plugin/app/service/foo3/foo3.js | 2 +- test/fixtures/apps/locals/app/router.js | 14 +- .../apps/logger-reload/app/controller/home.js | 2 +- .../apps/logrotator-app/app/router.js | 2 +- .../apps/middlewares/app/controller/error.js | 4 +- .../apps/middlewares/app/controller/home.js | 4 +- test/fixtures/apps/middlewares/app/router.js | 2 - .../apps/middlewares/config/config.default.js | 2 - .../apps/middlewares/config/plugin.js | 5 + .../apps/multipart/app/controller/home.js | 4 +- .../apps/multipart/app/controller/upload.js | 4 +- .../apps/notfound-custom-404/app/router.js | 2 +- .../apps/onerror/app/controller/home.js | 6 +- .../apps/onerror/app/controller/user.js | 4 +- .../apps/override_method/app/router.js | 8 +- .../app/controller/home.js | 2 +- .../apps/reload-worker/app/controller/home.js | 2 +- .../reload-worker/app/controller/home1.js | 2 +- test/fixtures/apps/response/app/router.js | 26 +- .../apps/router-app/app/controller/locals.js | 6 +- .../apps/router-app/app/controller/members.js | 8 +- .../apps/router-app/app/controller/posts.js | 16 +- .../apps/secure-app/app/controller/index.js | 4 +- .../apps/service-app/app/controller/user.js | 4 +- .../apps/service-app/app/service/user.js | 4 +- .../app/router.js | 2 +- .../subdir-services/app/controller/home.js | 20 +- .../certify-personal/mobile-hi/do_certify.js | 4 +- .../subdir-services/app/service/cif/user.js | 4 +- .../subdir-services/app/service/foo/bar.js | 4 +- .../app/service/foo/subdir/bar.js | 4 +- .../app/service/foo/subdir1/subdir11/bar.js | 4 +- .../apps/subdir-services/app/service/user.js | 4 +- .../apps/tracer-demo/app/controller/home.js | 4 +- .../apps/view-render/app/controller/async.js | 15 +- .../view-render/app/controller/context.js | 4 +- .../apps/view-render/app/controller/csrf.js | 4 +- .../apps/view-render/app/controller/empty.js | 4 +- .../apps/view-render/app/controller/home.js | 4 +- .../apps/view-render/app/controller/inject.js | 8 +- .../apps/view-render/app/controller/locals.js | 4 +- .../apps/view-render/app/controller/nonce.js | 4 +- .../apps/view-render/app/controller/shtml.js | 4 +- .../apps/view-render/app/controller/sjs.js | 4 +- .../apps/view-render/app/controller/string.js | 4 +- .../apps/view-render/app/controller/xss.js | 4 +- .../watcher-development-app/app/router.js | 12 +- test/index.test-d.ts | 13 + test/index.test.js | 22 - test/index.test.ts | 33 + test/lib/agent.test.js | 4 +- test/lib/application.test.js | 6 +- ...{app_worker.test.js => app_worker.test.ts} | 58 +- test/lib/cluster/cluster-client-error.test.js | 2 +- test/lib/cluster/master.test.js | 8 +- test/lib/core/dnscache_httpclient.test.js | 4 +- test/lib/core/httpclient_tracer_demo.test.js | 4 +- test/lib/core/loader/load_router.test.js | 26 - test/lib/core/loader/load_router.test.ts | 22 + test/lib/core/logger.test.js | 18 +- test/lib/core/messenger/ipc.test.js | 4 +- test/lib/egg.test.js | 14 +- test/lib/plugins/multipart.test.js | 4 +- test/lib/plugins/schedule.test.js | 2 +- test/lib/plugins/watcher.test.js | 10 +- test/lib/start.test.js | 78 - test/lib/start.test.ts | 78 + test/urllib.test.ts | 33 + test/utils.js | 130 -- test/utils.ts | 152 ++ tsconfig.json | 10 + 259 files changed, 4958 insertions(+), 4533 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 agent.js delete mode 100644 app/extend/context.js delete mode 100644 app/extend/response.js 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 create mode 100644 example/helloworld-commonjs/app/controller/home.js create mode 100644 example/helloworld-commonjs/app/router.js create mode 100644 example/helloworld-commonjs/config/config.default.js create mode 100644 example/helloworld-commonjs/config/plugin.js create mode 100644 example/helloworld-commonjs/index.js create mode 100644 example/helloworld-commonjs/package.json create mode 100644 example/helloworld-typescript/app.ts create mode 100644 example/helloworld-typescript/app/controller/home.ts create mode 100644 example/helloworld-typescript/app/middleware/hello.ts create mode 100644 example/helloworld-typescript/app/router.ts create mode 100644 example/helloworld-typescript/config/config.default.ts create mode 100644 example/helloworld-typescript/package.json create mode 100644 example/helloworld-typescript/tsconfig.json create mode 100644 index-old.d.ts delete mode 100644 index.d.ts delete mode 100644 index.js delete mode 100644 index.test-d.ts delete mode 100644 lib/agent.js delete mode 100644 lib/core/base_context_class.js delete mode 100644 lib/core/base_context_logger.js delete mode 100644 lib/core/base_hook_class.js delete mode 100644 lib/core/context_httpclient.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 100644 lib/core/logger.js delete mode 100644 lib/core/messenger/index.js delete mode 100644 lib/core/messenger/ipc.js delete mode 100644 lib/core/utils.js delete mode 100644 lib/loader/agent_worker_loader.js delete mode 100644 lib/loader/app_worker_loader.js delete mode 100644 lib/loader/index.js delete mode 100644 lib/start.js delete mode 100755 scripts/commits.sh create mode 100644 src/agent.ts create mode 100644 src/app/extend/context.ts create mode 100644 src/app/extend/context.types.ts rename app/extend/helper.js => src/app/extend/helper.ts (64%) rename app/extend/request.js => src/app/extend/request.ts (62%) create mode 100644 src/app/extend/response.ts create mode 100644 src/app/middleware/body_parser.ts rename app/middleware/meta.js => src/app/middleware/meta.ts (53%) rename app/middleware/notfound.js => src/app/middleware/notfound.ts (72%) 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 (90%) 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 (96%) create mode 100644 src/index.ts create mode 100644 src/lib/agent.ts rename lib/application.js => src/lib/application.ts (61%) create mode 100644 src/lib/core/base_context_class.ts create mode 100644 src/lib/core/base_context_logger.ts create mode 100644 src/lib/core/base_hook_class.ts create mode 100644 src/lib/core/context_httpclient.ts create mode 100644 src/lib/core/httpclient.ts create mode 100644 src/lib/core/logger.ts create mode 100644 src/lib/core/messenger/IMessenger.ts create mode 100644 src/lib/core/messenger/index.ts create mode 100644 src/lib/core/messenger/ipc.ts rename lib/core/messenger/local.js => src/lib/core/messenger/local.ts (58%) rename lib/core/singleton.js => src/lib/core/singleton.ts (54%) create mode 100644 src/lib/core/utils.ts rename lib/egg.js => src/lib/egg.ts (51%) create mode 100644 src/lib/egg.types.ts create mode 100644 src/lib/loader/AgentWorkerLoader.ts create mode 100644 src/lib/loader/AppWorkerLoader.ts create mode 100644 src/lib/loader/EggApplicationLoader.ts create mode 100644 src/lib/loader/index.ts create mode 100644 src/lib/start.ts create mode 100644 src/lib/type.ts create mode 100644 src/lib/utils.ts create mode 100644 src/urllib.ts delete mode 100644 test/agent.test.js create mode 100644 test/agent.test.ts rename test/app/extend/{agent.test.js => agent.test.ts} (65%) rename test/app/extend/{application.test.js => application.test.ts} (73%) rename test/app/extend/{context.jsonp.test.js => context.jsonp.test.ts} (84%) rename test/app/extend/{context.test.js => context.test.ts} (75%) rename test/app/extend/{helper.test.js => helper.test.ts} (88%) rename test/app/extend/{request.test.js => request.test.ts} (89%) delete mode 100644 test/app/extend/response.test.js create mode 100644 test/app/extend/response.test.ts rename test/app/middleware/{body_parser.test.js => body_parser.test.ts} (83%) rename test/app/middleware/{meta.test.js => meta.test.ts} (69%) rename test/app/middleware/{notfound.test.js => notfound.test.ts} (84%) rename test/app/middleware/{override_method.test.js => override_method.test.ts} (84%) rename test/app/middleware/{site_file.test.js => site_file.test.ts} (75%) delete mode 100644 test/asyncSupport.test.js create mode 100644 test/asyncSupport.test.ts create mode 100644 test/fixtures/apps/app-router/config/plugin.js create mode 100644 test/fixtures/apps/favicon-buffer/app/controller/home.js create mode 100644 test/fixtures/apps/favicon-buffer/app/router.js create mode 100644 test/fixtures/apps/favicon-buffer/config/config.default.js create mode 100644 test/fixtures/apps/favicon-buffer/package.json create mode 100644 test/fixtures/apps/middlewares/config/plugin.js create mode 100644 test/index.test-d.ts delete mode 100644 test/index.test.js create mode 100644 test/index.test.ts rename test/lib/cluster/{app_worker.test.js => app_worker.test.ts} (74%) delete mode 100644 test/lib/core/loader/load_router.test.js create mode 100644 test/lib/core/loader/load_router.test.ts delete mode 100644 test/lib/start.test.js create mode 100644 test/lib/start.test.ts create mode 100644 test/urllib.test.ts delete mode 100644 test/utils.js create mode 100644 test/utils.ts create mode 100644 tsconfig.json 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/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/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/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/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/.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 96f32102b8..b2749a812d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,5 @@ name: Release + on: push: branches: [ skip-releases ] @@ -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/.gitignore b/.gitignore index 8fe303468d..bcd29b3a8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/node_modules +node_modules coverage *.log npm-debug.log @@ -36,3 +36,5 @@ site/dist .umi-production .vercel package-lock.json +.tshy* +dist diff --git a/README.md b/README.md index 602a358413..b2580de174 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ English | [简体中文](./README.zh-CN.md) [![NPM version](https://img.shields.io/npm/v/egg.svg?style=flat-square)](https://npmjs.org/package/egg) [![NPM quality](http://npm.packagequality.com/shield/egg.svg?style=flat-square)](http://packagequality.com/#?package=egg) [![NPM download](https://img.shields.io/npm/dm/egg.svg?style=flat-square)](https://npmjs.org/package/egg) +[![Node.js Version](https://img.shields.io/node/v/egg.svg?style=flat)](https://nodejs.org/en/download/) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Feggjs%2Fegg.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Feggjs%2Fegg?ref=badge_shield) [![Continuous Integration](https://github.com/eggjs/egg/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg/actions?query=branch%3Amaster) @@ -14,7 +15,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 +62,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..51fcb0a260 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,6 +7,7 @@ [![NPM version](https://img.shields.io/npm/v/egg.svg?style=flat-square)](https://npmjs.org/package/egg) [![NPM quality](http://npm.packagequality.com/shield/egg.svg?style=flat-square)](http://packagequality.com/#?package=egg) [![NPM download](https://img.shields.io/npm/dm/egg.svg?style=flat-square)](https://npmjs.org/package/egg) +[![Node.js Version](https://img.shields.io/node/v/egg.svg?style=flat)](https://nodejs.org/en/download/) [![Continuous Integration](https://github.com/eggjs/egg/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg/actions?query=branch%3Amaster) [![Test coverage](https://img.shields.io/codecov/c/github/eggjs/egg.svg?style=flat-square)](https://codecov.io/gh/eggjs/egg) @@ -25,11 +26,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/agent.js b/agent.js deleted file mode 100644 index 17fbdc32eb..0000000000 --- a/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/app/extend/context.js b/app/extend/context.js deleted file mode 100644 index 8c5277235a..0000000000 --- a/app/extend/context.js +++ /dev/null @@ -1,285 +0,0 @@ -'use strict'; - -const { performance } = require('perf_hooks'); -const delegate = require('delegates'); -const { assign } = require('utility'); -const eggUtils = require('egg-core').utils; - -const HELPER = Symbol('Context#helper'); -const LOCALS = Symbol('Context#locals'); -const LOCALS_LIST = Symbol('Context#localsList'); -const COOKIES = Symbol('Context#cookies'); -const CONTEXT_LOGGERS = Symbol('Context#logger'); -const CONTEXT_HTTPCLIENT = Symbol('Context#httpclient'); -const CONTEXT_ROUTER = Symbol('Context#router'); - -const proto = module.exports = { - - /** - * Get the current visitor's cookies. - */ - get cookies() { - if (!this[COOKIES]) { - this[COOKIES] = new this.app.ContextCookies(this, this.app.keys, this.app.config.cookies); - } - return this[COOKIES]; - }, - - /** - * Get a wrapper httpclient instance contain ctx in the hold request process - * - * @return {ContextHttpClient} the wrapper httpclient instance - */ - get httpclient() { - if (!this[CONTEXT_HTTPCLIENT]) { - this[CONTEXT_HTTPCLIENT] = new this.app.ContextHttpClient(this); - } - return this[CONTEXT_HTTPCLIENT]; - }, - - /** - * Shortcut for httpclient.curl - * - * @function Context#curl - * @param {String|Object} url - request url address. - * @param {Object} [options] - options for request. - * @return {Object} see {@link ContextHttpClient#curl} - */ - curl(url, options) { - return this.httpclient.curl(url, options); - }, - - /** - * Alias to {@link Application#router} - * - * @member {Router} Context#router - * @since 1.0.0 - * @example - * ```js - * this.router.pathFor('post', { id: 12 }); - * ``` - */ - get router() { - if (!this[CONTEXT_ROUTER]) { - this[CONTEXT_ROUTER] = this.app.router; - } - return this[CONTEXT_ROUTER]; - }, - - /** - * Set router to Context, only use on EggRouter - * @param {EggRouter} val router instance - */ - set router(val) { - this[CONTEXT_ROUTER] = val; - }, - - /** - * Get helper instance from {@link Application#Helper} - * - * @member {Helper} Context#helper - * @since 1.0.0 - */ - get helper() { - if (!this[HELPER]) { - this[HELPER] = new this.app.Helper(this); - } - return this[HELPER]; - }, - - /** - * Wrap app.loggers with context infomation, - * if a custom logger is defined by naming aLogger, then you can `ctx.getLogger('aLogger')` - * - * @param {String} name - logger name - * @return {Logger} logger - */ - getLogger(name) { - if (this.app.config.logger.enableFastContextLogger) { - return this.app.getLogger(name); - } - let cache = this[CONTEXT_LOGGERS]; - if (!cache) { - cache = this[CONTEXT_LOGGERS] = {}; - } - - // read from cache - if (cache[name]) return cache[name]; - - // get no exist logger - const appLogger = this.app.getLogger(name); - if (!appLogger) return null; - - // write to cache - cache[name] = new this.app.ContextLogger(this, appLogger); - return cache[name]; - }, - - /** - * Logger for Application, wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#logger - * @since 1.0.0 - * @example - * ```js - * this.logger.info('some request data: %j', this.request.body); - * this.logger.warn('WARNING!!!!'); - * ``` - */ - get logger() { - return this.getLogger('logger'); - }, - - /** - * Logger for frameworks and plugins, - * wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#coreLogger - * @since 1.0.0 - */ - get coreLogger() { - return this.getLogger('coreLogger'); - }, - - /** - * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, - * which will be used as data when view is rendering. - * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. - * - * when you set locals, only object is available - * - * ```js - * this.locals = { - * a: 1 - * }; - * this.locals = { - * b: 1 - * }; - * this.locals.c = 1; - * console.log(this.locals); - * { - * a: 1, - * b: 1, - * c: 1, - * }; - * ``` - * - * `ctx.locals` has cache, it only merges `app.locals` once in one request. - * - * @member {Object} Context#locals - */ - get locals() { - if (!this[LOCALS]) { - this[LOCALS] = assign({}, this.app.locals); - } - if (this[LOCALS_LIST] && this[LOCALS_LIST].length) { - assign(this[LOCALS], this[LOCALS_LIST]); - this[LOCALS_LIST] = null; - } - return this[LOCALS]; - }, - - set locals(val) { - if (!this[LOCALS_LIST]) { - this[LOCALS_LIST] = []; - } - this[LOCALS_LIST].push(val); - }, - - /** - * alias to {@link Context#locals}, compatible with koa that use this variable - * @member {Object} state - * @see Context#locals - */ - get state() { - return this.locals; - }, - - set state(val) { - this.locals = val; - }, - - /** - * Run async function in the background - * @param {Function} scope - the first args is ctx - * ```js - * this.body = 'hi'; - * - * this.runInBackground(async ctx => { - * await ctx.mysql.query(sql); - * await ctx.curl(url); - * }); - * ``` - */ - runInBackground(scope) { - // try to use custom function name first - /* istanbul ignore next */ - const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true); - scope._name = taskName; - this._runInBackground(scope); - }, - - // let plugins or frameworks to reuse _runInBackground in some cases. - // e.g.: https://github.com/eggjs/egg-mock/pull/78 - _runInBackground(scope) { - const ctx = this; - const start = performance.now(); - /* istanbul ignore next */ - const taskName = scope._name || scope.name || eggUtils.getCalleeFromStack(true); - // use setImmediate to ensure all sync logic will run async - return new Promise(resolve => setImmediate(resolve)) - // use app.toAsyncFunction to support both generator function and async function - .then(() => ctx.app.toAsyncFunction(scope)(ctx)) - .then(() => { - ctx.coreLogger.info('[egg:background] task:%s success (%dms)', - taskName, Math.floor((performance.now() - start) * 1000) / 1000); - }) - .catch(err => { - // background task process log - ctx.coreLogger.info('[egg:background] task:%s fail (%dms)', - taskName, Math.floor((performance.now() - start) * 1000) / 1000); - - // emit error when promise catch, and set err.runInBackground flag - err.runInBackground = true; - ctx.app.emit('error', err, ctx); - }); - }, -}; - -/** - * Context delegation. - */ - -delegate(proto, 'request') - /** - * @member {Boolean} Context#acceptJSON - * @see Request#acceptJSON - * @since 1.0.0 - */ - .getter('acceptJSON') - /** - * @member {Array} Context#queries - * @see Request#queries - * @since 1.0.0 - */ - .getter('queries') - /** - * @member {Boolean} Context#accept - * @see Request#accept - * @since 1.0.0 - */ - .getter('accept') - /** - * @member {string} Context#ip - * @see Request#ip - * @since 1.0.0 - */ - .access('ip'); - -delegate(proto, 'response') - /** - * @member {Number} Context#realStatus - * @see Response#realStatus - * @since 1.0.0 - */ - .access('realStatus'); diff --git a/app/extend/response.js b/app/extend/response.js deleted file mode 100644 index 643d53f610..0000000000 --- a/app/extend/response.js +++ /dev/null @@ -1,101 +0,0 @@ -'use strict'; - -const getType = require('cache-content-type'); -const isJSON = require('koa-is-json'); - -const REAL_STATUS = Symbol('Context#realStatus'); - -module.exports = { - - /** - * Get or set the length of content. - * - * For Get: If the original content length is null or undefined, it will read out - * the body's content length as the return value. - * - * @member {Number} Response#type - * @param {Number} len The content-length to be set. - */ - set length(len) { - // copy from koa - // change header name to lower case - this.set('content-length', len); - }, - - get length() { - // copy from koa - const len = this.header['content-length']; - const body = this.body; - - if (len == null) { - if (!body) return; - if (typeof body === 'string') return Buffer.byteLength(body); - if (Buffer.isBuffer(body)) return body.length; - if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body)); - return; - } - - return parseInt(len, 10); - }, - - /** - * Get or set the content-type. - * - * For Set: If type is null or undefined, this property will be removed. - * - * For Get: If the value is null or undefined, an empty string will be returned; - * if you have multiple values seperated by `;`, ONLY the first one will be returned. - * - * @member {String} Response#type - * @param {String} type The content-type to be set. - */ - set type(type) { - // copy from koa - // Different: - // - change header name to lower case - type = getType(type); - if (type) { - this.set('content-type', type); - } else { - this.remove('content-type'); - } - }, - - get type() { - // copy from koa - const type = this.get('content-type'); - if (!type) return ''; - return type.split(';')[0]; - }, - - /** - * Get or set a real status code. - * - * e.g.: Using 302 status redirect to the global error page - * instead of show current 500 status page. - * And access log should save 500 not 302, - * then the `realStatus` can help us find out the real status code. - * @member {Number} Response#realStatus - * @return {Number} The status code to be set. - */ - get realStatus() { - if (this[REAL_STATUS]) { - return this[REAL_STATUS]; - } - return this.status; - }, - - /** - * Set a real status code. - * - * e.g.: Using 302 status redirect to the global error page - * instead of show current 500 status page. - * And access log should save 500 not 302, - * then the `realStatus` can help us find out the real status code. - * @member {Number} Response#realStatus - * @param {Number} status The status code to be set. - */ - set realStatus(status) { - this[REAL_STATUS] = status; - }, -}; 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 { + app.get('/', 'home.index'); +}; diff --git a/example/helloworld-commonjs/config/config.default.js b/example/helloworld-commonjs/config/config.default.js new file mode 100644 index 0000000000..19944025f1 --- /dev/null +++ b/example/helloworld-commonjs/config/config.default.js @@ -0,0 +1 @@ +exports.keys = 'hello world'; diff --git a/example/helloworld-commonjs/config/plugin.js b/example/helloworld-commonjs/config/plugin.js new file mode 100644 index 0000000000..d3697a6580 --- /dev/null +++ b/example/helloworld-commonjs/config/plugin.js @@ -0,0 +1,5 @@ +exports.schedule = { + enable: false, +}; + +exports.logrotator = false; diff --git a/example/helloworld-commonjs/index.js b/example/helloworld-commonjs/index.js new file mode 100644 index 0000000000..4e2277d3bd --- /dev/null +++ b/example/helloworld-commonjs/index.js @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { once } = require('node:events'); +const { Application } = require('../../dist/commonjs/index'); + +const app = new Application({ + baseDir: process.cwd(), + mode: 'single', +}); + +async function main() { + await app.ready(); + console.log('egg app ready'); + + const server = app.listen(7001); + await once(server, 'listening'); + console.log(`egg app server listening at http://localhost:${server.address().port}`); +} + +main(); diff --git a/example/helloworld-commonjs/package.json b/example/helloworld-commonjs/package.json new file mode 100644 index 0000000000..b880871fa3 --- /dev/null +++ b/example/helloworld-commonjs/package.json @@ -0,0 +1,4 @@ +{ + "name": "hello-commonjs", + "type": "commonjs" +} diff --git a/example/helloworld-typescript/app.ts b/example/helloworld-typescript/app.ts new file mode 100644 index 0000000000..713985a8ba --- /dev/null +++ b/example/helloworld-typescript/app.ts @@ -0,0 +1,34 @@ +import { ILifecycleBoot, Application } from 'egg'; + +export default class AppBootHook implements ILifecycleBoot { + private readonly app: Application; + + constructor(app: Application) { + this.app = app; + } + + async didLoad() { + console.error('didLoad'); + // Ready to call configDidLoad, + // Config, plugin files are referred, + // this is the last chance to modify the config. + // throw new Error('Method not implemented.'); + } + + async willReady() { + // All plugins have started, can do some thing before app ready + } + + async didReady() { + // Worker is ready, can do some things + // don't need to block the app boot process + } + + async serverDidReady() { + // Server is listening. + } + + async beforeClose() { + // Do some thing before app close. + } +} diff --git a/example/helloworld-typescript/app/controller/home.ts b/example/helloworld-typescript/app/controller/home.ts new file mode 100644 index 0000000000..b8f7863b6a --- /dev/null +++ b/example/helloworld-typescript/app/controller/home.ts @@ -0,0 +1,7 @@ +import { Controller } from 'egg'; + +export default class HomeController extends Controller { + async index() { + this.ctx.body = 'Hello EggJS 🥚🥚🥚🥚'; + } +} diff --git a/example/helloworld-typescript/app/middleware/hello.ts b/example/helloworld-typescript/app/middleware/hello.ts new file mode 100644 index 0000000000..e70448695a --- /dev/null +++ b/example/helloworld-typescript/app/middleware/hello.ts @@ -0,0 +1,13 @@ +import { MiddlewareFunc } from '../../../../src/index.js'; + +export const hello: MiddlewareFunc = async (ctx, next) => { + ctx.body = 'Hello World!'; + console.log(ctx.app.type, ctx.app.server, ctx.app.ctxStorage.getStore()?.performanceStarttime); + console.log(ctx.performanceStarttime); + const res = await ctx.curl('https://eggjs.org'); + console.log(res.status); + + // egg watcher + // console.log('egg watcher', ctx.app.watcher); + await next(); +}; diff --git a/example/helloworld-typescript/app/router.ts b/example/helloworld-typescript/app/router.ts new file mode 100644 index 0000000000..6dc26961bf --- /dev/null +++ b/example/helloworld-typescript/app/router.ts @@ -0,0 +1,5 @@ +import { Application } from 'egg'; + +export default (app: Application) => { + app.get('/', 'home.index'); +}; diff --git a/example/helloworld-typescript/config/config.default.ts b/example/helloworld-typescript/config/config.default.ts new file mode 100644 index 0000000000..fcbc3e97e8 --- /dev/null +++ b/example/helloworld-typescript/config/config.default.ts @@ -0,0 +1,7 @@ +import { EggAppConfig } from 'egg'; + +export default () => { + return { + keys: '123456', + } as Partial; +}; diff --git a/example/helloworld-typescript/package.json b/example/helloworld-typescript/package.json new file mode 100644 index 0000000000..a23e274286 --- /dev/null +++ b/example/helloworld-typescript/package.json @@ -0,0 +1,10 @@ +{ + "name": "helloworld-typescript", + "type": "module", + "egg": { + "typescript": true + }, + "dependencies": { + "egg": "beta" + } +} diff --git a/example/helloworld-typescript/tsconfig.json b/example/helloworld-typescript/tsconfig.json new file mode 100644 index 0000000000..ff41b73422 --- /dev/null +++ b/example/helloworld-typescript/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/index-old.d.ts b/index-old.d.ts new file mode 100644 index 0000000000..b187499352 --- /dev/null +++ b/index-old.d.ts @@ -0,0 +1,1268 @@ +// import accepts = require('accepts'); +// import { AsyncLocalStorage } from 'async_hooks'; +// import { EventEmitter } from 'events'; +// import { Readable } from 'stream'; +// import { Socket } from 'net'; +// import { IncomingMessage, ServerResponse } from 'http'; +// import KoaApplication = require('koa'); +// import KoaRouter = require('koa-router'); +// import { +// EggLogger as Logger, +// EggLoggers, +// LoggerLevel as EggLoggerLevel, +// EggLoggersOptions, +// EggLoggerOptions, +// EggContextLogger, +// } from 'egg-logger'; +// import { +// RequestOptions2 as RequestOptionsOld, +// HttpClientResponse as HttpClientResponseOld, +// } from 'urllib'; +// import { +// RequestURL as HttpClientRequestURL, +// RequestOptions as HttpClientRequestOptions, +// HttpClientResponse, +// } from 'urllib-next'; +// import { +// EggCoreBase, +// FileLoaderOption, +// EggLoader as CoreLoader, +// EggCoreOptions as CoreOptions, +// EggLoaderOptions as CoreLoaderOptions, +// BaseContextClass as CoreBaseContextClass, +// } from 'egg-core'; +// import EggCookies = require('egg-cookies'); +// import 'egg-onerror'; +// import 'egg-session'; +// import 'egg-i18n'; +// import '@eggjs/watcher'; +// import 'egg-multipart'; +// import 'egg-security'; +// import 'egg-development'; +// import 'egg-logrotator'; +// import '@eggjs/schedule'; +// import 'egg-static'; +// import 'egg-jsonp'; +// import 'egg-view'; + +// declare module 'egg' { +// export type EggLogger = Logger; +// // plain object +// type PlainObject = { [key: string]: T }; + +// // Remove specific property from the specific class +// type RemoveSpecProp = Pick>; + +// // Usage: +// // ```ts +// // import { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse } from 'egg'; +// // async function request(url: HttpClientRequestURL, options: HttpClientRequestOptions): Promise { +// // return await app.httpclient.request(url, options); +// // } +// // ``` +// export { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse }; +// // Compatible with both urllib@2 and urllib@3 RequestOptions to request +// export interface EggHttpClient extends EventEmitter { +// request(url: HttpClientRequestURL): Promise | HttpClientResponse>; +// request(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): +// Promise | HttpClientResponse>; +// curl(url: HttpClientRequestURL): Promise | HttpClientResponse>; +// curl(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): +// Promise | HttpClientResponse>; +// } + +// interface EggHttpConstructor { +// new(app: Application): EggHttpClient; +// } + +// export interface EggContextHttpClient extends EggHttpClient { } +// interface EggContextHttpClientConstructor { +// new(ctx: Context): EggContextHttpClient; +// } + +// /** +// * BaseContextClass is a base class that can be extended, +// * it's instantiated in context level, +// * {@link Helper}, {@link Service} is extending it. +// */ +// export class BaseContextClass extends CoreBaseContextClass { // tslint:disable-line +// /** +// * logger +// */ +// protected logger: EggLogger; +// } + +// export class Boot { +// /** +// * logger +// * @member {EggLogger} +// */ +// protected logger: EggLogger; + +// /** +// * The configuration of application +// * @member {EggAppConfig} +// */ +// protected config: EggAppConfig; + +// /** +// * The instance of agent +// * @member {Agent} +// */ +// protected agent: Agent; + +// /** +// * The instance of app +// * @member {Application} +// */ +// protected app: Application; +// } + +// export type RequestArrayBody = any[]; +// export type RequestObjectBody = PlainObject; +// export interface Request extends KoaApplication.Request { // tslint:disable-line +// /** +// * detect if response should be json +// * 1. url path ends with `.json` +// * 2. response type is set to json +// * 3. detect by request accept header +// * +// * @member {Boolean} Request#acceptJSON +// * @since 1.0.0 +// */ +// acceptJSON: boolean; + +// /** +// * Request remote IPv4 address +// * @member {String} Request#ip +// * @example +// * ```js +// * this.request.ip +// * => '127.0.0.1' +// * => '111.10.2.1' +// * ``` +// */ +// ip: string; + +// /** +// * Get all pass through ip addresses from the request. +// * Enable only on `app.config.proxy = true` +// * +// * @member {Array} Request#ips +// * @example +// * ```js +// * this.request.ips +// * => ['100.23.1.2', '201.10.10.2'] +// * ``` +// */ +// ips: string[]; + +// protocol: string; + +// /** +// * get params pass by querystring, all value are Array type. {@link Request#query} +// * @member {Array} Request#queries +// * @example +// * ```js +// * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val +// * this.queries +// * => +// * { +// * "a": ["b", "c"], +// * "o[foo]": ["bar"], +// * "b[]": ["1", "2"], +// * "e": ["val"] +// * } +// * ``` +// */ +// queries: PlainObject; + +// /** +// * get params pass by querystring, all value are String type. +// * @member {Object} Request#query +// * @example +// * ```js +// * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 +// * this.query +// * => { 'name': 'Foo', 'age': 20 } +// * +// * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val +// * this.query +// * => +// * { +// * "a": "b", +// * "o[foo]": "bar", +// * "b[]": "1", +// * "e": "val" +// * } +// * ``` +// */ +// query: PlainObject; + +// body: any; +// } + +// export interface Response extends KoaApplication.Response { // tslint:disable-line +// /** +// * read response real status code. +// * +// * e.g.: Using 302 status redirect to the global error page +// * instead of show current 500 status page. +// * And access log should save 500 not 302, +// * then the `realStatus` can help us find out the real status code. +// * @member {Number} Context#realStatus +// */ +// realStatus: number; +// body: ResponseBodyT; +// } + +// export type LoggerLevel = EggLoggerLevel; + + +// /** +// * egg app info +// * @example +// * ```js +// * // config/config.default.ts +// * import { EggAppInfo } from 'egg'; +// * +// * export default (appInfo: EggAppInfo) => { +// * return { +// * keys: appInfo.name + '123456', +// * }; +// * } +// * ``` +// */ +// export interface EggAppInfo { +// pkg: any; // package.json +// name: string; // the application name from package.json +// baseDir: string; // current directory of application +// env: EggEnvType; // equals to serverEnv +// HOME: string; // home directory of the OS +// root: string; // baseDir when local and unittest, HOME when other environment +// } + +// type IgnoreItem = string | RegExp | ((ctx: Context) => boolean); +// type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; + +// /** Custom Loader Configuration */ +// export interface CustomLoaderConfig extends RemoveSpecProp { +// /** +// * 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 HttpClientBaseConfig { +// /** Whether use http keepalive */ +// keepAlive?: boolean; +// /** Free socket after keepalive timeout */ +// freeSocketKeepAliveTimeout?: number; +// /** Free socket after request timeout */ +// freeSocketTimeout?: number; +// /** Request timeout */ +// timeout?: number; +// /** Determines how many concurrent sockets the agent can have open per origin */ +// maxSockets?: number; +// /** The maximum number of sockets that will be left open in the free state */ +// maxFreeSockets?: number; +// } + +// /** HttpClient config */ +// export interface HttpClientConfig extends HttpClientBaseConfig { +// /** http.Agent */ +// httpAgent?: HttpClientBaseConfig; +// /** https.Agent */ +// httpsAgent?: HttpClientBaseConfig; +// /** Default request args for httpclient */ +// request?: HttpClientRequestOptions | RequestOptionsOld; +// /** Whether enable dns cache */ +// enableDNSCache?: boolean; +// /** Enable proxy request, default is false. */ +// enableProxy?: boolean; +// /** proxy agent uri or options, default is null. */ +// proxy?: string | { [key: string]: any }; +// /** DNS cache lookup interval */ +// dnsCacheLookupInterval?: number; +// /** DNS cache max age */ +// dnsCacheMaxLength?: number; +// /** use urllib@3 HttpClient */ +// useHttpClientNext?: boolean; +// } + +// export interface EggAppConfig { +// workerStartTimeout: number; +// baseDir: string; +// middleware: 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'; +// }; + +// /** +// * logger options +// * @member Config#logger +// * @property {String} dir - directory of log files +// * @property {String} encoding - log file encloding, 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, defaults to NONE elsewise +// * @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} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to false +// */ +// logger: EggLoggerConfig; + +// /** custom logger of egg */ +// customLogger: { +// [key: string]: EggLoggerOptions; +// }; + +// /** Configuration of httpclient in egg. */ +// httpclient: HttpClientConfig; + +// 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; +// }; + +// /** +// * 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; + +// /** +// * 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 seperated by . +// */ +// keys: string; + +// /** +// * The name of the application +// */ +// name: string; + +// /** +// * package.json +// */ +// pkg: any; + +// 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: PlainObject; + +// watcher: PlainObject; + +// 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; + +// [prop: string]: any; +// } + +// export interface ClientErrorResponse { +// body: string | Buffer; +// status: number; +// headers: { [key: string]: string }; +// } + +// export interface Router extends Omit, 'url'> { +// /** +// * restful router api +// */ +// resources(name: string, prefix: string, ...middleware: any[]): Router; + +// /** +// * @param {String} name - Router name +// * @param {Object} [params] - more parameters +// * @example +// * ```js +// * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) +// * => /posts/1/edit?name=foo&page=2 +// * router.url('posts', { name: 'foo&1', page: 2 }) +// * => /posts?name=foo%261&page=2 +// * ``` +// * @return {String} url by path name and query params. +// * @since 1.0.0 +// */ +// url(name: string, params?: any): string; +// /** +// * Alias for the url method +// */ +// pathFor(name: string, params?: any): string; +// methods: string[]; +// } + +// export interface EggApplication extends Omit, 'ctxStorage' | 'currentContext'> { +// /** +// * HttpClient instance +// */ +// httpclient: EggHttpClient; + +// /** +// * Logger for Application, wrapping app.coreLogger with context infomation +// * +// * @member {ContextLogger} Context#logger +// * @since 1.0.0 +// * @example +// * ```js +// * this.logger.info('some request data: %j', this.request.body); +// * this.logger.warn('WARNING!!!!'); +// * ``` +// */ +// logger: EggLogger; + +// /** +// * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web +// */ +// coreLogger: EggLogger; + +// /** +// * All loggers contain logger, coreLogger and customLogger +// */ +// loggers: EggLoggers; + +// /** +// * messenger instance +// */ +// messenger: Messenger; + +// /** +// * get router +// */ +// router: Router; + +// /** +// * create a singleton instance +// */ +// addSingleton(name: string, create: any): void; + +// runSchedule(schedulePath: string, ...args: any[]): Promise; + +// /** +// * http request helper base on httpclient, it will auto save httpclient log. +// * Keep the same api with httpclient.request(url, args). +// * See https://github.com/node-modules/urllib#api-doc for more details. +// */ +// curl: EggHttpClient['request']; + +// /** +// * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical +// */ +// getLogger(name: string): EggLogger; + +// /** +// * print the infomation when console.log(app) +// */ +// inspect(): any; + +// /** +// * Alias to Router#url +// */ +// url(name: string, params: any): any; + +// /** +// * Create an anonymous context, the context isn't request level, so the request is mocked. +// * then you can use context level API like `ctx.service` +// * @member {String} EggApplication#createAnonymousContext +// * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. +// * @return {Context} context +// */ +// createAnonymousContext(req?: Request): Context; + +// /** +// * export context base classes, let framework can impl sub class and over context extend easily. +// */ +// ContextCookies: typeof EggCookies; +// ContextLogger: typeof EggContextLogger; +// ContextHttpClient: EggContextHttpClientConstructor; +// HttpClient: EggHttpConstructor; +// Subscription: typeof Subscription; +// Controller: typeof Controller; +// Service: typeof Service; +// } + +// // compatible +// export class EggApplication { +// constructor(options?: CoreOptions); +// } + +// export type RouterPath = string | RegExp; + +// export class Application extends EggApplication { +// /** +// * global locals for view +// * @see Context#locals +// */ +// locals: IApplicationLocals; + +// /** +// * HTTP get method +// */ +// get(path: RouterPath, fn: string): void; +// get(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP post method +// */ +// post(path: RouterPath, fn: string): void; +// post(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP put method +// */ +// put(path: RouterPath, fn: string): void; +// put(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP patch method +// */ +// patch(path: RouterPath, fn: string): void; +// patch(path: RouterPath, ...middleware: any[]): void; + +// /** +// * HTTP delete method +// */ +// delete(path: RouterPath, fn: string): void; +// delete(path: RouterPath, ...middleware: any[]): void; + +// /** +// * restful router api +// */ +// resources(name: string, prefix: string, fn: string): Router; +// resources(path: string, prefix: string, ...middleware: any[]): Router; + +// redirect(path: string, redirectPath: string): void; + +// controller: IController; + +// middleware: KoaApplication.Middleware[] & IMiddleware; + +// /** +// * Run async function in the background +// * @see Context#runInBackground +// * @param {Function} scope - the first args is an anonymous ctx +// */ +// runInBackground(scope: (ctx: Context) => void): void; + +// /** +// * Run async function in the anonymous context scope +// * @see Context#runInAnonymousContextScope +// * @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. +// */ +// runInAnonymousContextScope(scope: (ctx: Context) => Promise, req?: Request): Promise; + +// /** +// * Get current execute ctx async local storage +// * @return {AsyncLocalStorage} localStorage - store current execute Context +// */ +// get ctxStorage(): AsyncLocalStorage; + +// /** +// * Get current execute ctx, maybe undefined +// * @return {Context} ctx - current execute Context +// */ +// get currentContext(): Context; +// } + +// export interface IApplicationLocals extends PlainObject { } + +// export interface FileStream extends Readable { // tslint:disable-line +// fields: any; + +// filename: string; + +// fieldname: string; + +// mime: string; + +// mimeType: string; + +// transferEncoding: string; + +// encoding: string; + +// truncated: boolean; +// } + +// interface GetFileStreamOptions { +// requireFile?: boolean; // required file submit, default is true +// defCharset?: string; +// limits?: { +// fieldNameSize?: number; +// fieldSize?: number; +// fields?: number; +// fileSize?: number; +// files?: number; +// parts?: number; +// headerPairs?: number; +// }; +// checkFile?( +// fieldname: string, +// file: any, +// filename: string, +// encoding: string, +// mimetype: string +// ): void | Error; +// } + +// /** +// * KoaApplication's Context will carry the default 'cookie' property in +// * the egg's Context interface, which is wrong here because we have our own +// * special properties (e.g: encrypted). So we must remove this property and +// * create our own with the same name. +// * @see https://github.com/eggjs/egg/pull/2958 +// * +// * However, the latest version of Koa has "[key: string]: any" on the +// * context, and there'll be a type error for "keyof koa.Context". +// * So we have to directly inherit from "KoaApplication.BaseContext" and +// * rewrite all the properties to be compatible with types in Koa. +// * @see https://github.com/eggjs/egg/pull/3329 +// */ +// export interface Context extends KoaApplication.BaseContext { +// [key: string]: any; +// body: ResponseBodyT; + +// app: Application; + +// // properties of koa.Context +// req: IncomingMessage; +// res: ServerResponse; +// originalUrl: string; +// respond?: boolean; + +// service: IService; + +// request: Request; + +// response: Response; + +// // The new 'cookies' instead of Koa's. +// cookies: EggCookies; + +// helper: IHelper; + +// /** +// * Resource Parameters +// * @example +// * ##### ctx.params.id {string} +// * +// * `GET /api/users/1` => `'1'` +// * +// * ##### ctx.params.ids {Array} +// * +// * `GET /api/users/1,2,3` => `['1', '2', '3']` +// * +// * ##### ctx.params.fields {Array} +// * +// * Expect request return data fields, for example +// * `GET /api/users/1?fields=name,title` => `['name', 'title']`. +// * +// * ##### ctx.params.data {Object} +// * +// * Tht request data object +// * +// * ##### ctx.params.page {Number} +// * +// * Page number, `GET /api/users?page=10` => `10` +// * +// * ##### ctx.params.per_page {Number} +// * +// * The number of every page, `GET /api/users?per_page=20` => `20` +// */ +// params: any; + +// /** +// * @see Request#query +// */ +// query: PlainObject; + +// /** +// * @see Request#queries +// */ +// queries: PlainObject; + +// /** +// * @see Request#accept +// */ +// accept: accepts.Accepts; + +// /** +// * @see Request#acceptJSON +// */ +// acceptJSON: boolean; + +// /** +// * @see Request#ip +// */ +// ip: string; + +// /** +// * @see Response#realStatus +// */ +// realStatus: number; + +// /** +// * Set the ctx.body.data value +// * +// * @member {Object} Context#data= +// * @example +// * ```js +// * ctx.data = { +// * id: 1, +// * name: 'fengmk2' +// * }; +// * ``` +// * +// * will get responce +// * +// * ```js +// * HTTP/1.1 200 OK +// * +// * { +// * "data": { +// * "id": 1, +// * "name": "fengmk2" +// * } +// * } +// * ``` +// */ +// data: any; + +// /** +// * set ctx.body.meta value +// * +// * @example +// * ```js +// * ctx.meta = { +// * count: 100 +// * }; +// * ``` +// * will get responce +// * +// * ```js +// * HTTP/1.1 200 OK +// * +// * { +// * "meta": { +// * "count": 100 +// * } +// * } +// * ``` +// */ +// meta: any; + +// /** +// * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, +// * which will be used as data when view is rendering. +// * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. +// * +// * when you set locals, only object is available +// * +// * ```js +// * this.locals = { +// * a: 1 +// * }; +// * this.locals = { +// * b: 1 +// * }; +// * this.locals.c = 1; +// * console.log(this.locals); +// * { +// * a: 1, +// * b: 1, +// * c: 1, +// * }; +// * ``` +// * +// * `ctx.locals` has cache, it only merges `app.locals` once in one request. +// * +// * @member {Object} Context#locals +// */ +// locals: IApplicationLocals & IContextLocals; + +// /** +// * alias to {@link locals}, compatible with koa that use this variable +// */ +// state: any; + +// /** +// * Logger for Application, wrapping app.coreLogger with context infomation +// * +// * @member {ContextLogger} Context#logger +// * @since 1.0.0 +// * @example +// * ```js +// * this.logger.info('some request data: %j', this.request.body); +// * this.logger.warn('WARNING!!!!'); +// * ``` +// */ +// logger: EggLogger; + +// /** +// * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical +// */ +// getLogger(name: string): EggLogger; + +// /** +// * Request start time +// */ +// starttime: number; + +// /** +// * Request start timer using `performance.now()` +// */ +// performanceStarttime?: number; + +// /** +// * http request helper base on httpclient, it will auto save httpclient log. +// * Keep the same api with httpclient.request(url, args). +// * See https://github.com/node-modules/urllib#api-doc for more details. +// */ +// curl: EggHttpClient['request']; + +// __(key: string, ...values: string[]): string; +// gettext(key: string, ...values: string[]): string; + +// /** +// * get upload file stream +// * @example +// * ```js +// * const stream = await this.getFileStream(); +// * // get other fields +// * console.log(stream.fields); +// * ``` +// * @function Context#getFileStream +// * @param {Object} options +// * @return {ReadStream} stream +// * @since 1.0.0 +// */ +// getFileStream(options?: GetFileStreamOptions): Promise; + +// /** +// * @see Responce.redirect +// */ +// redirect(url: string, alt?: string): void; + +// httpclient: EggContextHttpClient; +// } + +// export interface IContextLocals extends PlainObject { } + +// export class Controller extends BaseContextClass { } + +// export class Service extends BaseContextClass { } + +// export class Subscription extends BaseContextClass { } + +// /** +// * The empty interface `IService` is a placeholder, for egg +// * to auto injection service to ctx.service +// * +// * @example +// * +// * import { Service } from 'egg'; +// * class FooService extends Service { +// * async bar() {} +// * } +// * +// * declare module 'egg' { +// * export interface IService { +// * foo: FooService; +// * } +// * } +// * +// * Now I can get ctx.service.foo at controller and other service file. +// */ +// export interface IService extends PlainObject { } // tslint:disable-line + +// export interface IController extends PlainObject { } // tslint:disable-line + +// export interface IMiddleware extends PlainObject { } // tslint:disable-line + +// export interface IHelper extends PlainObject, BaseContextClass { +// /** +// * Generate URL path(without host) for route. Takes the route name and a map of named params. +// * @function Helper#pathFor +// * @param {String} name - Router Name +// * @param {Object} params - Other params +// * +// * @example +// * ```js +// * app.get('home', '/index.htm', 'home.index'); +// * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) +// * => /index.htm?by=recent&limit=20 +// * ``` +// * @return {String} url path(without host) +// */ +// pathFor(name: string, params?: PlainObject): string; + +// /** +// * Generate full URL(with host) for route. Takes the route name and a map of named params. +// * @function Helper#urlFor +// * @param {String} name - Router name +// * @param {Object} params - Other params +// * @example +// * ```js +// * app.get('home', '/index.htm', 'home.index'); +// * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) +// * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 +// * ``` +// * @return {String} full url(with host) +// */ +// urlFor(name: string, params?: PlainObject): string; +// } + +// // egg env type +// export type EggEnvType = 'local' | 'unittest' | 'prod' | string; + +// /** +// * plugin config item interface +// */ +// export interface IEggPluginItem { +// env?: EggEnvType[]; +// path?: string; +// package?: string; +// enable?: boolean; +// } + +// export type EggPluginItem = IEggPluginItem | boolean; + +// /** +// * build-in plugin list +// */ +// export interface EggPlugin { +// [key: string]: EggPluginItem | undefined; +// onerror?: EggPluginItem; +// session?: EggPluginItem; +// i18n?: EggPluginItem; +// watcher?: EggPluginItem; +// multipart?: EggPluginItem; +// security?: EggPluginItem; +// development?: EggPluginItem; +// logrotator?: EggPluginItem; +// schedule?: EggPluginItem; +// static?: EggPluginItem; +// jsonp?: EggPluginItem; +// view?: EggPluginItem; +// } + +// /** +// * Singleton instance in Agent Worker, extend {@link EggApplication} +// */ +// export class Agent extends EggApplication { +// } + +// export interface ClusterOptions { +// /** specify framework that can be absolute path or npm package */ +// framework?: string; +// /** directory of application, default to `process.cwd()` */ +// baseDir?: string; +// /** customized plugins, for unittest */ +// plugins?: object | null; +// /** numbers of app workers, default to `os.cpus().length` */ +// workers?: number; +// /** listening port, default to 7001(http) or 8443(https) */ +// port?: number; +// /** https or not */ +// https?: boolean; +// /** ssl key */ +// key?: string; +// /** ssl cert */ +// cert?: string; +// [prop: string]: any; +// } + +// export function startCluster(options: ClusterOptions, callback: (...args: any[]) => any): void; + +// 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; +// } + +// export function start(options?: StartOptions): Promise; + +// /** +// * Powerful Partial, Support adding ? modifier to a mapped property in deep level +// * @example +// * import { PowerPartial, EggAppConfig } from 'egg'; +// * +// * // { view: { defaultEngines: string } } => { view?: { defaultEngines?: string } } +// * type EggConfig = PowerPartial +// */ +// export type PowerPartial = { +// [U in keyof T]?: T[U] extends object +// ? PowerPartial +// : T[U] +// }; + +// // send data can be number|string|boolean|object but not Set|Map +// export interface Messenger extends EventEmitter { +// /** +// * broadcast to all agent/app processes including itself +// */ +// broadcast(action: string, data: any): void; + +// /** +// * send to agent from the app, +// * send to an random app from the agent +// */ +// sendRandom(action: string, data: any): void; + +// /** +// * send to specified process +// */ +// sendTo(pid: number, action: string, data: any): void; + +// /** +// * send to agent from the app, +// * send to itself from the agent +// */ +// sendToAgent(action: string, data: any): void; + +// /** +// * send to all app including itself from the app, +// * send to all app from the agent +// */ +// sendToApp(action: string, data: any): void; +// } + +// // compatible +// export interface EggLoaderOptions extends CoreLoaderOptions { } +// export interface EggLoader extends CoreLoader { } + +// /** +// * App worker process Loader, will load plugins +// * @see https://github.com/eggjs/egg-core +// */ +// export class AppWorkerLoader extends CoreLoader { +// loadConfig(): void; +// load(): void; +// } + +// /** +// * Agent worker process loader +// * @see https://github.com/eggjs/egg-loader +// */ +// export class AgentWorkerLoader extends CoreLoader { +// loadConfig(): void; +// load(): void; +// } + +// export interface IBoot { +// /** +// * Ready to call configDidLoad, +// * Config, plugin files are referred, +// * this is the last chance to modify the config. +// */ +// configWillLoad?(): void; + +// /** +// * Config, plugin files have loaded +// */ +// configDidLoad?(): void; + +// /** +// * All files have loaded, start plugin here +// */ +// didLoad?(): Promise; + +// /** +// * All plugins have started, can do some thing before app ready +// */ +// willReady?(): Promise; + +// /** +// * Worker is ready, can do some things, +// * don't need to block the app boot +// */ +// didReady?(): Promise; + +// /** +// * Server is listening +// */ +// serverDidReady?(): Promise; + +// /** +// * Do some thing before app close +// */ +// beforeClose?(): Promise; +// } + +// export interface Singleton { +// get(id: string): T; +// } +// } diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 1dbf24df55..0000000000 --- a/index.d.ts +++ /dev/null @@ -1,1283 +0,0 @@ -import accepts = require('accepts'); -import { AsyncLocalStorage } from 'async_hooks'; -import { EventEmitter } from 'events' -import { Readable } from 'stream'; -import { Socket } from 'net'; -import { IncomingMessage, ServerResponse } from 'http'; -import KoaApplication = require('koa'); -import KoaRouter = require('koa-router'); -import { - EggLogger as Logger, - EggLoggers, - LoggerLevel as EggLoggerLevel, - EggLoggersOptions, - EggLoggerOptions, - EggContextLogger, -} from 'egg-logger'; -import { - RequestOptions2 as RequestOptionsOld, - HttpClientResponse as HttpClientResponseOld, -} from 'urllib'; -import { - RequestURL as HttpClientRequestURL, - RequestOptions as HttpClientRequestOptions, - HttpClientResponse, -} from 'urllib-next'; -import { - EggCoreBase, - FileLoaderOption, - EggLoader as CoreLoader, - EggCoreOptions as CoreOptions, - EggLoaderOptions as CoreLoaderOptions, - BaseContextClass as CoreBaseContextClass, -} from 'egg-core'; -import EggCookies = require('egg-cookies'); -import 'egg-onerror'; -import 'egg-session'; -import 'egg-i18n'; -import 'egg-watcher'; -import 'egg-multipart'; -import 'egg-security'; -import 'egg-development'; -import 'egg-logrotator'; -import 'egg-schedule'; -import 'egg-static'; -import 'egg-jsonp'; -import 'egg-view'; - -declare module 'egg' { - export type EggLogger = Logger; - // plain object - type PlainObject = { [key: string]: T }; - - // Remove specific property from the specific class - type RemoveSpecProp = Pick>; - - // Usage: - // ```ts - // import { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse } from 'egg'; - // async function request(url: HttpClientRequestURL, options: HttpClientRequestOptions): Promise { - // return await app.httpclient.request(url, options); - // } - // ``` - export { HttpClientRequestURL, HttpClientRequestOptions, HttpClientResponse }; - // Compatible with both urllib@2 and urllib@3 RequestOptions to request - export interface EggHttpClient extends EventEmitter { - request(url: HttpClientRequestURL): Promise | HttpClientResponse>; - request(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; - curl(url: HttpClientRequestURL): Promise | HttpClientResponse>; - curl(url: HttpClientRequestURL, options: RequestOptionsOld | HttpClientRequestOptions): - Promise | HttpClientResponse>; - } - - interface EggHttpConstructor { - new(app: Application): EggHttpClient; - } - - export interface EggContextHttpClient extends EggHttpClient { } - interface EggContextHttpClientConstructor { - new(ctx: Context): EggContextHttpClient; - } - - /** - * BaseContextClass is a base class that can be extended, - * it's instantiated in context level, - * {@link Helper}, {@link Service} is extending it. - */ - export class BaseContextClass extends CoreBaseContextClass { // tslint:disable-line - /** - * logger - */ - protected logger: EggLogger; - } - - export class Boot { - /** - * logger - * @member {EggLogger} - */ - protected logger: EggLogger; - - /** - * The configuration of application - * @member {EggAppConfig} - */ - protected config: EggAppConfig; - - /** - * The instance of agent - * @member {Agent} - */ - protected agent: Agent; - - /** - * The instance of app - * @member {Application} - */ - protected app: Application; - } - - export type RequestArrayBody = any[]; - export type RequestObjectBody = PlainObject; - export interface Request extends KoaApplication.Request { // tslint:disable-line - /** - * detect if response should be json - * 1. url path ends with `.json` - * 2. response type is set to json - * 3. detect by request accept header - * - * @member {Boolean} Request#acceptJSON - * @since 1.0.0 - */ - acceptJSON: boolean; - - /** - * Request remote IPv4 address - * @member {String} Request#ip - * @example - * ```js - * this.request.ip - * => '127.0.0.1' - * => '111.10.2.1' - * ``` - */ - ip: string; - - /** - * Get all pass through ip addresses from the request. - * Enable only on `app.config.proxy = true` - * - * @member {Array} Request#ips - * @example - * ```js - * this.request.ips - * => ['100.23.1.2', '201.10.10.2'] - * ``` - */ - ips: string[]; - - protocol: string; - - /** - * get params pass by querystring, all value are Array type. {@link Request#query} - * @member {Array} Request#queries - * @example - * ```js - * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val - * this.queries - * => - * { - * "a": ["b", "c"], - * "o[foo]": ["bar"], - * "b[]": ["1", "2"], - * "e": ["val"] - * } - * ``` - */ - queries: PlainObject; - - /** - * get params pass by querystring, all value are String type. - * @member {Object} Request#query - * @example - * ```js - * GET http://127.0.0.1:7001?name=Foo&age=20&age=21 - * this.query - * => { 'name': 'Foo', 'age': 20 } - * - * GET http://127.0.0.1:7001?a=b&a=c&o[foo]=bar&b[]=1&b[]=2&e=val - * this.query - * => - * { - * "a": "b", - * "o[foo]": "bar", - * "b[]": "1", - * "e": "val" - * } - * ``` - */ - query: PlainObject; - - body: any; - } - - export interface Response extends KoaApplication.Response { // tslint:disable-line - /** - * read response real status code. - * - * e.g.: Using 302 status redirect to the global error page - * instead of show current 500 status page. - * And access log should save 500 not 302, - * then the `realStatus` can help us find out the real status code. - * @member {Number} Context#realStatus - */ - realStatus: number; - body: ResponseBodyT; - } - - export type LoggerLevel = EggLoggerLevel; - - - /** - * egg app info - * @example - * ```js - * // config/config.default.ts - * import { EggAppInfo } from 'egg'; - * - * export default (appInfo: EggAppInfo) => { - * return { - * keys: appInfo.name + '123456', - * }; - * } - * ``` - */ - export interface EggAppInfo { - pkg: any; // package.json - name: string; // the application name from package.json - baseDir: string; // current directory of application - env: EggEnvType; // equals to serverEnv - HOME: string; // home directory of the OS - root: string; // baseDir when local and unittest, HOME when other environment - } - - 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 { - /** - * 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 HttpClientBaseConfig { - /** Whether use http keepalive */ - keepAlive?: boolean; - /** Free socket after keepalive timeout */ - freeSocketKeepAliveTimeout?: number; - /** Free socket after request timeout */ - freeSocketTimeout?: number; - /** Request timeout */ - timeout?: number; - /** Determines how many concurrent sockets the agent can have open per origin */ - maxSockets?: number; - /** The maximum number of sockets that will be left open in the free state */ - maxFreeSockets?: number; - } - - /** HttpClient config */ - export interface HttpClientConfig extends HttpClientBaseConfig { - /** http.Agent */ - httpAgent?: HttpClientBaseConfig; - /** https.Agent */ - httpsAgent?: HttpClientBaseConfig; - /** Default request args for httpclient */ - request?: HttpClientRequestOptions | RequestOptionsOld; - /** Whether enable dns cache */ - enableDNSCache?: boolean; - /** Enable proxy request, default is false. */ - enableProxy?: boolean; - /** proxy agent uri or options, default is null. */ - proxy?: string | { [key: string]: any }; - /** DNS cache lookup interval */ - dnsCacheLookupInterval?: number; - /** DNS cache max age */ - dnsCacheMaxLength?: number; - /** use urllib@3 HttpClient */ - useHttpClientNext?: boolean; - } - - export interface EggAppConfig { - workerStartTimeout: number; - baseDir: string; - middleware: 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'; - }; - - /** - * logger options - * @member Config#logger - * @property {String} dir - directory of log files - * @property {String} encoding - log file encloding, 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, defaults to NONE elsewise - * @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: EggLoggerConfig; - - /** custom logger of egg */ - customLogger: { - [key: string]: EggLoggerOptions; - }; - - /** Configuration of httpclient in egg. */ - httpclient: HttpClientConfig; - - 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; - }; - - /** - * 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; - - /** - * 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 seperated by . - */ - keys: string; - - /** - * The name of the application - */ - name: string; - - /** - * package.json - */ - pkg: any; - - 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: PlainObject; - - watcher: PlainObject; - - 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; - - [prop: string]: any; - } - - export interface ClientErrorResponse { - body: string | Buffer; - status: number; - headers: { [key: string]: string }; - } - - export interface Router extends Omit, 'url'> { - /** - * restful router api - */ - resources(name: string, prefix: string, ...middleware: any[]): Router; - - /** - * @param {String} name - Router name - * @param {Object} [params] - more parameters - * @example - * ```js - * router.url('edit_post', { id: 1, name: 'foo', page: 2 }) - * => /posts/1/edit?name=foo&page=2 - * router.url('posts', { name: 'foo&1', page: 2 }) - * => /posts?name=foo%261&page=2 - * ``` - * @return {String} url by path name and query params. - * @since 1.0.0 - */ - url(name: string, params?: any): string; - /** - * Alias for the url method - */ - pathFor(name: string, params?: any): string; - methods: string[]; - } - - export interface EggApplication extends Omit, 'ctxStorage' | 'currentContext'> { - /** - * HttpClient instance - */ - httpclient: EggHttpClient; - - /** - * Logger for Application, wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#logger - * @since 1.0.0 - * @example - * ```js - * this.logger.info('some request data: %j', this.request.body); - * this.logger.warn('WARNING!!!!'); - * ``` - */ - logger: EggLogger; - - /** - * core logger for framework and plugins, log file is $HOME/logs/{appname}/egg-web - */ - coreLogger: EggLogger; - - /** - * All loggers contain logger, coreLogger and customLogger - */ - loggers: EggLoggers; - - /** - * messenger instance - */ - messenger: Messenger; - - /** - * get router - */ - router: Router; - - /** - * create a singleton instance - */ - addSingleton(name: string, create: any): void; - - runSchedule(schedulePath: string, ...args: any[]): Promise; - - /** - * http request helper base on httpclient, it will auto save httpclient log. - * Keep the same api with httpclient.request(url, args). - * See https://github.com/node-modules/urllib#api-doc for more details. - */ - curl: EggHttpClient['request']; - - /** - * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical - */ - getLogger(name: string): EggLogger; - - /** - * print the infomation when console.log(app) - */ - inspect(): any; - - /** - * Alias to Router#url - */ - url(name: string, params: any): any; - - /** - * Create an anonymous context, the context isn't request level, so the request is mocked. - * then you can use context level API like `ctx.service` - * @member {String} EggApplication#createAnonymousContext - * @param {Request} req - if you want to mock request like querystring, you can pass an object to this function. - * @return {Context} context - */ - createAnonymousContext(req?: Request): Context; - - /** - * export context base classes, let framework can impl sub class and over context extend easily. - */ - ContextCookies: typeof EggCookies; - ContextLogger: typeof EggContextLogger; - ContextHttpClient: EggContextHttpClientConstructor; - HttpClient: EggHttpConstructor; - Subscription: typeof Subscription; - Controller: typeof Controller; - Service: typeof Service; - } - - // compatible - export class EggApplication { - constructor(options?: CoreOptions); - } - - export type RouterPath = string | RegExp; - - export class Application extends EggApplication { - /** - * global locals for view - * @see Context#locals - */ - locals: IApplicationLocals; - - /** - * HTTP get method - */ - get(path: RouterPath, fn: string): void; - get(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP post method - */ - post(path: RouterPath, fn: string): void; - post(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP put method - */ - put(path: RouterPath, fn: string): void; - put(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP patch method - */ - patch(path: RouterPath, fn: string): void; - patch(path: RouterPath, ...middleware: any[]): void; - - /** - * HTTP delete method - */ - delete(path: RouterPath, fn: string): void; - delete(path: RouterPath, ...middleware: any[]): void; - - /** - * restful router api - */ - resources(name: string, prefix: string, fn: string): Router; - resources(path: string, prefix: string, ...middleware: any[]): Router; - - redirect(path: string, redirectPath: string): void; - - controller: IController; - - middleware: KoaApplication.Middleware[] & IMiddleware; - - /** - * Run async function in the background - * @see Context#runInBackground - * @param {Function} scope - the first args is an anonymous ctx - */ - runInBackground(scope: (ctx: Context) => void): void; - - /** - * Run async function in the anonymous context scope - * @see Context#runInAnonymousContextScope - * @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. - */ - runInAnonymousContextScope(scope: (ctx: Context) => Promise, req?: Request): Promise; - - /** - * Get current execute ctx async local storage - * @returns {AsyncLocalStorage} localStorage - store current execute Context - */ - get ctxStorage(): AsyncLocalStorage; - - /** - * Get current execute ctx, maybe undefined - * @returns {Context} ctx - current execute Context - */ - get currentContext(): Context; - } - - export interface IApplicationLocals extends PlainObject { } - - export interface FileStream extends Readable { // tslint:disable-line - fields: any; - - filename: string; - - fieldname: string; - - mime: string; - - mimeType: string; - - transferEncoding: string; - - encoding: string; - - truncated: boolean; - } - - interface GetFileStreamOptions { - requireFile?: boolean; // required file submit, default is true - defCharset?: string; - limits?: { - fieldNameSize?: number; - fieldSize?: number; - fields?: number; - fileSize?: number; - files?: number; - parts?: number; - headerPairs?: number; - }; - checkFile?( - fieldname: string, - file: any, - filename: string, - encoding: string, - mimetype: string - ): void | Error; - } - - /** - * KoaApplication's Context will carry the default 'cookie' property in - * the egg's Context interface, which is wrong here because we have our own - * special properties (e.g: encrypted). So we must remove this property and - * create our own with the same name. - * @see https://github.com/eggjs/egg/pull/2958 - * - * However, the latest version of Koa has "[key: string]: any" on the - * context, and there'll be a type error for "keyof koa.Context". - * So we have to directly inherit from "KoaApplication.BaseContext" and - * rewrite all the properties to be compatible with types in Koa. - * @see https://github.com/eggjs/egg/pull/3329 - */ - export interface Context extends KoaApplication.BaseContext { - [key: string]: any; - body: ResponseBodyT; - - app: Application; - - // properties of koa.Context - req: IncomingMessage; - res: ServerResponse; - originalUrl: string; - respond?: boolean; - - service: IService; - - request: Request; - - response: Response; - - // The new 'cookies' instead of Koa's. - cookies: EggCookies; - - helper: IHelper; - - /** - * Resource Parameters - * @example - * ##### ctx.params.id {string} - * - * `GET /api/users/1` => `'1'` - * - * ##### ctx.params.ids {Array} - * - * `GET /api/users/1,2,3` => `['1', '2', '3']` - * - * ##### ctx.params.fields {Array} - * - * Expect request return data fields, for example - * `GET /api/users/1?fields=name,title` => `['name', 'title']`. - * - * ##### ctx.params.data {Object} - * - * Tht request data object - * - * ##### ctx.params.page {Number} - * - * Page number, `GET /api/users?page=10` => `10` - * - * ##### ctx.params.per_page {Number} - * - * The number of every page, `GET /api/users?per_page=20` => `20` - */ - params: any; - - /** - * @see Request#query - */ - query: PlainObject; - - /** - * @see Request#queries - */ - queries: PlainObject; - - /** - * @see Request#accept - */ - accept: accepts.Accepts; - - /** - * @see Request#acceptJSON - */ - acceptJSON: boolean; - - /** - * @see Request#ip - */ - ip: string; - - /** - * @see Response#realStatus - */ - realStatus: number; - - /** - * Set the ctx.body.data value - * - * @member {Object} Context#data= - * @example - * ```js - * ctx.data = { - * id: 1, - * name: 'fengmk2' - * }; - * ``` - * - * will get responce - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "data": { - * "id": 1, - * "name": "fengmk2" - * } - * } - * ``` - */ - data: any; - - /** - * set ctx.body.meta value - * - * @example - * ```js - * ctx.meta = { - * count: 100 - * }; - * ``` - * will get responce - * - * ```js - * HTTP/1.1 200 OK - * - * { - * "meta": { - * "count": 100 - * } - * } - * ``` - */ - meta: any; - - /** - * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, - * which will be used as data when view is rendering. - * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. - * - * when you set locals, only object is available - * - * ```js - * this.locals = { - * a: 1 - * }; - * this.locals = { - * b: 1 - * }; - * this.locals.c = 1; - * console.log(this.locals); - * { - * a: 1, - * b: 1, - * c: 1, - * }; - * ``` - * - * `ctx.locals` has cache, it only merges `app.locals` once in one request. - * - * @member {Object} Context#locals - */ - locals: IApplicationLocals & IContextLocals; - - /** - * alias to {@link locals}, compatible with koa that use this variable - */ - state: any; - - /** - * Logger for Application, wrapping app.coreLogger with context infomation - * - * @member {ContextLogger} Context#logger - * @since 1.0.0 - * @example - * ```js - * this.logger.info('some request data: %j', this.request.body); - * this.logger.warn('WARNING!!!!'); - * ``` - */ - logger: EggLogger; - - /** - * Get logger by name, it's equal to app.loggers['name'], but you can extend it with your own logical - */ - getLogger(name: string): EggLogger; - - /** - * Request start time - */ - starttime: number; - - /** - * Request start timer using `performance.now()` - */ - performanceStarttime?: number; - - /** - * http request helper base on httpclient, it will auto save httpclient log. - * Keep the same api with httpclient.request(url, args). - * See https://github.com/node-modules/urllib#api-doc for more details. - */ - curl: EggHttpClient['request']; - - __(key: string, ...values: string[]): string; - gettext(key: string, ...values: string[]): string; - - /** - * get upload file stream - * @example - * ```js - * const stream = await this.getFileStream(); - * // get other fields - * console.log(stream.fields); - * ``` - * @method Context#getFileStream - * @param {Object} options - * @return {ReadStream} stream - * @since 1.0.0 - */ - getFileStream(options?: GetFileStreamOptions): Promise; - - /** - * @see Responce.redirect - */ - redirect(url: string, alt?: string): void; - - httpclient: EggContextHttpClient; - } - - export interface IContextLocals extends PlainObject { } - - export class Controller extends BaseContextClass { } - - export class Service extends BaseContextClass { } - - export class Subscription extends BaseContextClass { } - - /** - * The empty interface `IService` is a placeholder, for egg - * to auto injection service to ctx.service - * - * @example - * - * import { Service } from 'egg'; - * class FooService extends Service { - * async bar() {} - * } - * - * declare module 'egg' { - * export interface IService { - * foo: FooService; - * } - * } - * - * Now I can get ctx.service.foo at controller and other service file. - */ - export interface IService extends PlainObject { } // tslint:disable-line - - export interface IController extends PlainObject { } // tslint:disable-line - - export interface IMiddleware extends PlainObject { } // tslint:disable-line - - 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 - * @param {String} name - Router Name - * @param {Object} params - Other params - * - * @example - * ```js - * app.get('home', '/index.htm', 'home.index'); - * ctx.helper.pathFor('home', { by: 'recent', limit: 20 }) - * => /index.htm?by=recent&limit=20 - * ``` - * @return {String} url path(without host) - */ - pathFor(name: string, params?: PlainObject): string; - - /** - * Generate full URL(with host) for route. Takes the route name and a map of named params. - * @method Helper#urlFor - * @param {String} name - Router name - * @param {Object} params - Other params - * @example - * ```js - * app.get('home', '/index.htm', 'home.index'); - * ctx.helper.urlFor('home', { by: 'recent', limit: 20 }) - * => http://127.0.0.1:7001/index.htm?by=recent&limit=20 - * ``` - * @return {String} full url(with host) - */ - urlFor(name: string, params?: PlainObject): string; - } - - // egg env type - export type EggEnvType = 'local' | 'unittest' | 'prod' | string; - - /** - * plugin config item interface - */ - export interface IEggPluginItem { - env?: EggEnvType[]; - path?: string; - package?: string; - enable?: boolean; - } - - export type EggPluginItem = IEggPluginItem | boolean; - - /** - * build-in plugin list - */ - export interface EggPlugin { - [key: string]: EggPluginItem | undefined; - onerror?: EggPluginItem; - session?: EggPluginItem; - i18n?: EggPluginItem; - watcher?: EggPluginItem; - multipart?: EggPluginItem; - security?: EggPluginItem; - development?: EggPluginItem; - logrotator?: EggPluginItem; - schedule?: EggPluginItem; - static?: EggPluginItem; - jsonp?: EggPluginItem; - view?: EggPluginItem; - } - - /** - * Singleton instance in Agent Worker, extend {@link EggApplication} - */ - export class Agent extends EggApplication { - } - - export interface ClusterOptions { - /** specify framework that can be absolute path or npm package */ - framework?: string; - /** directory of application, default to `process.cwd()` */ - baseDir?: string; - /** customized plugins, for unittest */ - plugins?: object | null; - /** numbers of app workers, default to `os.cpus().length` */ - workers?: number; - /** listening port, default to 7001(http) or 8443(https) */ - port?: number; - /** https or not */ - https?: boolean; - /** ssl key */ - key?: string; - /** ssl cert */ - cert?: string; - [prop: string]: any; - } - - export function startCluster(options: ClusterOptions, callback: (...args: any[]) => any): void; - - 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; - } - - export function start(options?: StartOptions): Promise - - /** - * Powerful Partial, Support adding ? modifier to a mapped property in deep level - * @example - * import { PowerPartial, EggAppConfig } from 'egg'; - * - * // { view: { defaultEngines: string } } => { view?: { defaultEngines?: string } } - * type EggConfig = PowerPartial - */ - export type PowerPartial = { - [U in keyof T]?: T[U] extends object - ? PowerPartial - : T[U] - }; - - // send data can be number|string|boolean|object but not Set|Map - export interface Messenger extends EventEmitter { - /** - * broadcast to all agent/app processes including itself - */ - broadcast(action: string, data: any): void; - - /** - * send to agent from the app, - * send to an random app from the agent - */ - sendRandom(action: string, data: any): void; - - /** - * send to specified process - */ - sendTo(pid: number, action: string, data: any): void; - - /** - * send to agent from the app, - * send to itself from the agent - */ - sendToAgent(action: string, data: any): void; - - /** - * send to all app including itself from the app, - * send to all app from the agent - */ - sendToApp(action: string, data: any): void; - } - - // compatible - export interface EggLoaderOptions extends CoreLoaderOptions { } - export interface EggLoader extends CoreLoader { } - - /** - * App worker process Loader, will load plugins - * @see https://github.com/eggjs/egg-core - */ - export class AppWorkerLoader extends CoreLoader { - loadConfig(): void; - load(): void; - } - - /** - * Agent worker process loader - * @see https://github.com/eggjs/egg-loader - */ - export class AgentWorkerLoader extends CoreLoader { - loadConfig(): void; - load(): void; - } - - export interface IBoot { - /** - * Ready to call configDidLoad, - * Config, plugin files are referred, - * this is the last chance to modify the config. - */ - configWillLoad?(): void; - - /** - * Config, plugin files have loaded - */ - configDidLoad?(): void; - - /** - * All files have loaded, start plugin here - */ - didLoad?(): Promise; - - /** - * All plugins have started, can do some thing before app ready - */ - willReady?(): Promise; - - /** - * Worker is ready, can do some things, - * don't need to block the app boot - */ - didReady?(): Promise; - - /** - * Server is listening - */ - serverDidReady?(): Promise; - - /** - * Do some thing before app close - */ - beforeClose?(): Promise; - } - - export interface Singleton { - get(id: string): T; - } -} diff --git a/index.js b/index.js deleted file mode 100644 index 7e3269eda3..0000000000 --- a/index.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @namespace Egg - */ - -/** - * Start egg application with cluster mode - * @since 1.0.0 - */ -exports.startCluster = require('egg-cluster').startCluster; - -/** - * Start egg application with single process mode - * @since 1.0.0 - */ -exports.start = require('./lib/start'); - -/** - * @member {Application} Egg#Application - * @since 1.0.0 - */ -exports.Application = require('./lib/application'); - -/** - * @member {Agent} Egg#Agent - * @since 1.0.0 - */ -exports.Agent = require('./lib/agent'); - -/** - * @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; - -/** - * @member {Controller} Egg#Controller - * @since 1.1.0 - */ -exports.Controller = require('./lib/core/base_context_class'); - -/** - * @member {Service} Egg#Service - * @since 1.1.0 - */ -exports.Service = require('./lib/core/base_context_class'); - -/** - * @member {Subscription} Egg#Subscription - * @since 1.10.0 - */ -exports.Subscription = require('./lib/core/base_context_class'); - -/** - * @member {BaseContextClass} Egg#BaseContextClass - * @since 1.2.0 - */ -exports.BaseContextClass = require('./lib/core/base_context_class'); - -/** - * @member {Boot} Egg#Boot - */ -exports.Boot = require('./lib/core/base_hook_class'); diff --git a/index.test-d.ts b/index.test-d.ts deleted file mode 100644 index 8f7bcd336e..0000000000 --- a/index.test-d.ts +++ /dev/null @@ -1 +0,0 @@ -import '.'; diff --git a/lib/agent.js b/lib/agent.js deleted file mode 100644 index bfb3edf59c..0000000000 --- a/lib/agent.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -const path = require('path'); -const ms = require('ms'); -const EggApplication = require('./egg'); -const AgentWorkerLoader = require('./loader').AgentWorkerLoader; - -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 { - /** - * @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) - ); - - // 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); - } - - get [EGG_LOADER]() { - return AgentWorkerLoader; - } - - get [EGG_PATH]() { - return path.join(__dirname, '..'); - } - - _wrapMessenger() { - for (const methodName of [ - 'broadcast', - 'sendTo', - 'sendToApp', - 'sendToAgent', - 'sendRandom', - ]) { - wrapMethod(methodName, this.messenger, this.coreLogger); - } - - function wrapMethod(methodName, messenger, logger) { - const originMethod = messenger[methodName]; - messenger[methodName] = function() { - 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 - ); - originMethod.apply(this, arguments); - }; - messenger.prependOnceListener('egg-ready', () => { - messenger[methodName] = originMethod; - }); - } - } - - close() { - process.removeListener('uncaughtException', this._uncaughtExceptionHandler); - clearInterval(this.agentAliveHandler); - return super.close(); - } -} - -module.exports = Agent; diff --git a/lib/core/base_context_class.js b/lib/core/base_context_class.js deleted file mode 100644 index 1016d60440..0000000000 --- a/lib/core/base_context_class.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const EggCoreBaseContextClass = require('egg-core').BaseContextClass; -const BaseContextLogger = require('./base_context_logger'); - -const LOGGER = Symbol('BaseContextClass#logger'); - -/** - * BaseContextClass is a base class that can be extended, - * it's instantiated in context level, - * {@link Helper}, {@link Service} is extending it. - */ -class BaseContextClass extends EggCoreBaseContextClass { - 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/lib/core/base_context_logger.js deleted file mode 100644 index 6c1dc2c8df..0000000000 --- a/lib/core/base_context_logger.js +++ /dev/null @@ -1,64 +0,0 @@ -const CALL = Symbol('BaseContextLogger#call'); - -class BaseContextLogger { - /** - * @class - * @param {Context} ctx - context instance - * @param {String} pathName - class path name - * @since 1.0.0 - */ - constructor(ctx, pathName) { - /** - * @member {Context} BaseContextLogger#ctx - * @since 1.2.0 - */ - this.ctx = ctx; - this.pathName = pathName; - } - - [CALL](method, args) { - // add `[${pathName}]` in log - if (this.pathName && typeof args[0] === 'string') { - args[0] = `[${this.pathName}] ${args[0]}`; - } - this.ctx.app.logger[method](...args); - } - - /** - * @member {Function} BaseContextLogger#debug - * @param {...any} args - log msg - * @since 1.2.0 - */ - debug(...args) { - this[CALL]('debug', args); - } - - /** - * @member {Function} BaseContextLogger#info - * @param {...any} args - log msg - * @since 1.2.0 - */ - info(...args) { - this[CALL]('info', args); - } - - /** - * @member {Function} BaseContextLogger#warn - * @param {...any} args - log msg - * @since 1.2.0 - */ - warn(...args) { - this[CALL]('warn', args); - } - - /** - * @member {Function} BaseContextLogger#error - * @param {...any} args - log msg - * @since 1.2.0 - */ - error(...args) { - this[CALL]('error', args); - } -} - -module.exports = BaseContextLogger; diff --git a/lib/core/base_hook_class.js b/lib/core/base_hook_class.js deleted file mode 100644 index 2bda13bf60..0000000000 --- a/lib/core/base_hook_class.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const INSTANCE = Symbol('BaseHookClass#instance'); - -class BaseHookClass { - - constructor(instance) { - this[INSTANCE] = instance; - } - - get logger() { - return this[INSTANCE].logger; - } - - get config() { - return this[INSTANCE].config; - } - - get app() { - assert(this[INSTANCE].type === 'application', 'agent boot should not use app instance'); - return this[INSTANCE]; - } - - get agent() { - assert(this[INSTANCE].type === 'agent', 'app boot should not use agent instance'); - return this[INSTANCE]; - } -} - -module.exports = BaseHookClass; diff --git a/lib/core/context_httpclient.js b/lib/core/context_httpclient.js deleted file mode 100644 index a1db7d26a5..0000000000 --- a/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/lib/core/dnscache_httpclient.js b/lib/core/dnscache_httpclient.js deleted file mode 100644 index ba5e4bcb45..0000000000 --- a/lib/core/dnscache_httpclient.js +++ /dev/null @@ -1,93 +0,0 @@ -const dns = require('node:dns').promises; -const LRU = require('ylru'); -const { assign } = require('utility'); -const HttpClient = require('./httpclient'); -const utils = require('./utils'); - -const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; -const DNSLOOKUP = Symbol('DNSCacheHttpClient#dnslookup'); -const UPDATE_DNS = Symbol('DNSCacheHttpClient#updateDNS'); - -class DNSCacheHttpClient extends HttpClient { - constructor(app) { - super(app); - this.dnsCacheLookupInterval = this.app.config.httpclient.dnsCacheLookupInterval; - this.dnsCache = new LRU(this.app.config.httpclient.dnsCacheMaxLength); - } - - async request(url, args) { - // disable dns cache in request by args handle - if (args && args.enableDNSCache === false) { - return await super.request(url, args); - } - const result = await this[DNSLOOKUP](url, args); - return await super.request(result.url, result.args); - } - - async [DNSLOOKUP](url, args) { - let parsed; - if (typeof url === 'string') { - parsed = utils.safeParseURL(url); - // invalid url or relative url - if (!parsed) return { url, args }; - } else { - parsed = url; - } - // hostname must exists - const hostname = parsed.hostname; - - // don't lookup when hostname is IP - if (hostname && IP_REGEX.test(hostname)) { - return { url, args }; - } - - args = args || {}; - args.headers = args.headers || {}; - // set when host header doesn't exist - if (!args.headers.host && !args.headers.Host) { - // host must combine with hostname:port, node won't use `parsed.host` - args.headers.host = parsed.port ? `${hostname}:${parsed.port}` : hostname; - } - - const record = this.dnsCache.get(hostname); - const now = Date.now(); - if (record) { - if (now - record.timestamp >= 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/lib/core/logger.js b/lib/core/logger.js deleted file mode 100644 index a38dbddcf5..0000000000 --- a/lib/core/logger.js +++ /dev/null @@ -1,35 +0,0 @@ -const { EggLoggers } = require('egg-logger'); -const { setCustomLogger } = require('onelogger'); - -module.exports = function createLoggers(app) { - const loggerConfig = app.config.logger; - loggerConfig.type = app.type; - loggerConfig.localStorage = app.ctxStorage; - - if (app.config.env === 'prod' && loggerConfig.level === 'DEBUG' && !loggerConfig.allowDebugAtProd) { - loggerConfig.level = 'INFO'; - } - - const loggers = new EggLoggers(app.config); - - // won't print to console after started, except for local and unittest - app.ready(() => { - if (loggerConfig.disableConsoleAfterReady) { - loggers.disableConsole(); - } - }); - - // set global logger - for (const loggerName of Object.keys(loggers)) { - setCustomLogger(loggerName, loggers[loggerName]); - } - // reset global logger on beforeClose hook - app.beforeClose(() => { - for (const loggerName of Object.keys(loggers)) { - setCustomLogger(loggerName, undefined); - } - }); - loggers.coreLogger.info('[egg:logger] init all loggers with options: %j', loggerConfig); - - return loggers; -}; diff --git a/lib/core/messenger/index.js b/lib/core/messenger/index.js deleted file mode 100644 index f819cfa917..0000000000 --- a/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/lib/core/messenger/ipc.js b/lib/core/messenger/ipc.js deleted file mode 100644 index e758d6dd2c..0000000000 --- a/lib/core/messenger/ipc.js +++ /dev/null @@ -1,141 +0,0 @@ -'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'); - -/** - * Communication between app worker and agent worker by IPC channel - */ -class Messenger extends EventEmitter { - - constructor() { - super(); - this.pid = String(process.pid); - // 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); - } - } - - /** - * Send message to all agent and app - * @param {String} action - message key - * @param {Object} data - message value - * @return {Messenger} this - */ - broadcast(action, data) { - debug('[%s] broadcast %s with %j', this.pid, action, data); - this.send(action, data, 'app'); - this.send(action, data, 'agent'); - return this; - } - - /** - * 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, action, data) { - debug('[%s] send %s with %j to %s', this.pid, action, data, pid); - sendmessage(process, { - action, - data, - receiverPid: String(pid), - }); - return this; - } - - /** - * 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, data) { - /* istanbul ignore if */ - if (!this.opids.length) return this; - const pid = random(this.opids); - this.sendTo(String(pid), action, data); - return this; - } - - /** - * send message to app - * @param {String} action - message key - * @param {Object} data - message value - * @return {Messenger} this - */ - sendToApp(action, data) { - debug('[%s] send %s with %j to all app', this.pid, action, data); - this.send(action, data, 'app'); - return this; - } - - /** - * send message to agent - * @param {String} action - message key - * @param {Object} data - message value - * @return {Messenger} this - */ - sendToAgent(action, data) { - debug('[%s] send %s with %j to all agent', this.pid, action, data); - this.send(action, data, 'agent'); - return this; - } - - /** - * @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, data, to) { - sendmessage(process, { - action, - data, - to, - }); - return this; - } - - _onMessage(message) { - if (message && is.string(message.action)) { - debug('[%s] got message %s with %j, receiverPid: %s', - this.pid, message.action, message.data, message.receiverPid); - this.emit(message.action, message.data); - } - } - - close() { - process.removeListener('message', this._onMessage); - this.removeAllListeners(); - } - - /** - * @function Messenger#on - * @param {String} action - message key - * @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/lib/core/utils.js b/lib/core/utils.js deleted file mode 100644 index 445dead72e..0000000000 --- a/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/lib/loader/agent_worker_loader.js b/lib/loader/agent_worker_loader.js deleted file mode 100644 index abf4c3bde1..0000000000 --- a/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/lib/loader/app_worker_loader.js b/lib/loader/app_worker_loader.js deleted file mode 100644 index f2de253de3..0000000000 --- a/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/lib/loader/index.js b/lib/loader/index.js deleted file mode 100644 index 8c4e7c979d..0000000000 --- a/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/lib/start.js b/lib/start.js deleted file mode 100644 index 478c086d13..0000000000 --- a/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/package.json b/package.json index bbf1bf0cf3..78bff2e611 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "egg", - "version": "3.24.1", + "version": "4.0.0-beta.11", + "engines": { + "node": ">= 18.19.0" + }, "publishConfig": { - "tag": "latest" + "tag": "beta" }, - "description": "A web framework's framework for Node.js", + "description": "A web application framework for Node.js", "keywords": [ "web", "app", @@ -16,120 +19,146 @@ "egg" ], "dependencies": { - "@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", + "@eggjs/cluster": "^3.0.0", + "@eggjs/cookies": "^3.0.0", + "@eggjs/core": "^6.2.5", + "@eggjs/schedule": "^5.0.2", + "@eggjs/utils": "^4.1.5", + "@eggjs/watcher": "^4.0.1", "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-development": "^3.0.0", "egg-errors": "^2.3.1", "egg-i18n": "^2.1.1", "egg-jsonp": "^2.0.0", - "egg-logger": "^3.0.1", + "egg-logger": "^3.6.0", "egg-logrotator": "^3.1.0", "egg-multipart": "^3.1.0", "egg-onerror": "^2.1.1", - "egg-schedule": "^4.0.0", "egg-security": "^3.0.0", "egg-session": "^3.3.0", "egg-static": "^2.2.0", "egg-view": "^2.1.3", - "egg-watcher": "^3.1.1", - "extend2": "^1.0.1", - "graceful": "^1.1.0", - "humanize-ms": "^1.2.1", + "extend2": "^4.0.0", + "graceful": "^2.0.0", + "humanize-ms": "^2.0.0", "is-type-of": "^2.1.0", "koa-bodyparser": "^4.4.1", - "koa-is-json": "^1.0.0", - "koa-override": "^3.0.0", - "ms": "^2.1.3", - "on-finished": "^2.4.1", + "koa-override": "^4.0.0", "onelogger": "^1.0.0", - "sendmessage": "^2.0.0", - "urllib": "^2.33.0", - "urllib-next": "npm:urllib@^3.22.4", + "performance-ms": "^1.1.0", + "sendmessage": "^3.0.1", + "urllib": "^4.6.11", "utility": "^2.1.0", "ylru": "^1.3.2" }, "devDependencies": { - "@eggjs/tsconfig": "^1.1.0", - "@types/node": "^20.1.2", - "@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", - "dumi": "^1.1.47", - "dumi-theme-egg": "^1.2.2", - "egg-bin": "^6.4.1", - "egg-mock": "^5.10.7", + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/bin": "^7.0.0", + "@eggjs/koa": "^2.19.1", + "@eggjs/mock": "^6.0.3", + "@eggjs/supertest": "^8.1.1", + "@eggjs/tsconfig": "1", + "@types/delegates": "^1.0.3", + "@types/koa-bodyparser": "^4.3.12", + "@types/mocha": "^10.0.7", + "@types/ms": "^0.7.34", + "@types/node": "22", + "address": "2", + "assert-file": "1", + "coffee": "5", + "cross-env": "7", "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", - "findlinks": "^2.2.0", - "formstream": "^1.1.1", - "jsdoc": "^3.6.11", - "koa": "^2.13.4", + "eslint": "8", + "eslint-config-egg": "14", + "formstream": "^1.5.1", "koa-static": "^5.0.0", - "node-libs-browser": "^2.2.1", + "mm": "^3.4.0", "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", + "rimraf": "6", + "runscript": "^2.0.1", "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", - "umi": "^3.5.36" + "tsd": "^0.31.2", + "tshy": "^3.0.2", + "tshy-after": "1", + "typescript": "5" }, - "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", + "clean": "rimraf dist", + "lint": "eslint src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix", + "test": "egg-bin test", + "test-local": "egg-bin test", + "test:changed": "egg-bin test --changed", + "preci": "npm run clean && npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && npm run clean", + "prepublishOnly": "tshy && tshy-after && attw --pack --profile node16", "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" }, - "engines": { - "node": ">= 14.20.0" + "license": "MIT", + "tnpm": { + "mode": "npm" + }, + "egg": { + "framework": true, + "exports": { + "import": "./dist/esm", + "require": "./dist/commonjs", + "typescript": "./src" + } + }, + "files": [ + "dist", + "src" + ], + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.ts", + "./urllib": "./src/urllib.ts", + "./package.json": "./package.json" + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + }, + "require": { + "types": "./dist/commonjs/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./urllib": { + "import": { + "types": "./dist/esm/urllib.d.ts", + "default": "./dist/esm/urllib.js" + }, + "require": { + "types": "./dist/commonjs/urllib.d.ts", + "default": "./dist/commonjs/urllib.js" + } + }, + "./package.json": "./package.json" }, - "license": "MIT" + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/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/site/docs/advanced/framework.md b/site/docs/advanced/framework.md index 3eb3e8c91f..3ffeaf818e 100644 --- a/site/docs/advanced/framework.md +++ b/site/docs/advanced/framework.md @@ -6,7 +6,7 @@ order: 3 If your team have met with these scenarios: - Each project contains the same configuration files that need to be copied every time, such as `gulpfile.js`, `webpack.config.js`. -- Each project has similiar dependencies. +- Each project has similar dependencies. - It's difficult to synchronize those projects based on the same configurations like those mentioned above once they have been optimized? If your team needs: @@ -16,7 +16,7 @@ If your team needs: - a unified [deployment plan](../core/deployment.md) keeping developers concentrate on code without paying attention to deployment details of connecting the framework and platforms. - a unified code style to decrease code's repetition and optimize code's appearance, which is important for a enterprise level framework. -To satisfy these demands, Egg endows developers with the capacity of `customazing a framework`. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Futhermore, Egg apply a quantity of coding conventions based on Koa. +To satisfy these demands, Egg endows developers with the capacity of `customizing a framework`. It is just an abstract layer, which can be constructed to a higher level framework, supporting inheritance of unlimited times. Furthermore, Egg apply a quantity of coding conventions based on Koa. Therefore, a uniform spec can be applied on projects in which the differentiation fulfilled in plugins. And the best practice summed from those projects can be continuously extracted from these plugins to the framework, which is available to other projects by just updating the dependencies' versions. @@ -30,7 +30,7 @@ They both are inherited from [EggCore](https://github.com/eggjs/egg-core), and A We could regard EggCore as the advanced version of Koa Application, which integrates built-in features such as [Loader](./loader.md)、[Router](../basics/router.md) and asynchronous launch. -``` +```bash Koa Application ^ EggCore @@ -92,7 +92,7 @@ To customize framework, Loader is required and has to be inherited from Egg Load If we consider a framework as a class, then Egg framework is the base class,and implementing a framework demands to implement entire APIs of Egg. -```js +```bash // package.json { "name": "yadan", @@ -122,7 +122,7 @@ module.exports = Object.assign(egg, { }); ``` -The name of framework, default as `egg`, is a indispensable option to launch an application, set by `egg.framwork` of `package.json`, then Loader loads the exported app of a module named it. +The name of framework, default as `egg`, is a indispensable option to launch an application, set by `egg.framework` of `package.json`, then Loader loads the exported app of a module named it. ```json { @@ -139,7 +139,7 @@ As a loadUnit of framework, yadan is going to load specific directories and file ### Principle of Framework Extension -The path of framework is set as a varible named as `Symbol.for('egg#eggPath')` to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessiable through the prototype chain. +The path of framework is set as a variable named as `Symbol.for('egg#eggPath')` to expose itself to Loader. Why? It seems that the simplest way is to pass a param to the constructor. The reason is to expose those paths of each level of inherited frameworks and reserve their sequences. Since Egg is a framework capable of unlimited inheritance, each layer has to designate their own eggPath so that all the eggPaths are accessible through the prototype chain. Given a triple-layer framework: department level > enterprise level > Egg @@ -173,7 +173,7 @@ These code are pseudocode to elaborate the framework's loading process, and we h ### Custom Agent -Egg's mutilprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similiar to Application, is also required to be implemented. +Egg's multiprocess model is composed of Application and Agent. Therefore Agent, another fundamental class similar to Application, is also required to be implemented. ```js // lib/framework.js @@ -207,12 +207,13 @@ module.exports = Object.assign(egg, { Loader, the core of the launch process, is capable of loading data code, adjusting loading orders or even strengthen regulation of code. -As the same as Egg-Path, Loader exposes itself at `Symbol.for('egg#loader')` to ensuer it's accessibility on prototype chain. +As the same as Egg-Path, Loader exposes itself at `Symbol.for('egg#loader')` to ensure it's accessibility on prototype chain. ```js // lib/framework.js const path = require('path'); const egg = require('egg'); + const EGG_PATH = Symbol.for('egg#eggPath'); class YadanAppWorkerLoader extends egg.AppWorkerLoader { @@ -236,16 +237,16 @@ class Application extends egg.Application { // rewrite Egg's Application module.exports = Object.assign(egg, { Application, - // custom Loader, a dependence of the high level frameword, needs to be exported. + // custom Loader, a dependence of the high level framework, needs to be exported. AppWorkerLoader: YadanAppWorkerLoader, }); ``` -AgentworkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at `agent.js` instand of `app.js`. +AgentWorkerLoader is not going to be described because of it's similarity of AppWorkerLoader, but be aware of it's located at `agent.js` instead of `app.js`. ## The principle of Launch -Many descriptions of launch process are scattered at [Mutilprocess Model](../core/cluster-and-ipc.md), [Loader](./loader.md) and [Plugin](./plugin.md), and here is a summarization. +Many descriptions of launch process are scattered at [Multiprocess Model](../core/cluster-and-ipc.md), [Loader](./loader.md) and [Plugin](./plugin.md), and here is a summarization. - `startCluster` is invoked with `baseDir` and `framework`, then Master process is launched. - Master forks a new process as Agent Worker @@ -253,22 +254,22 @@ Many descriptions of launch process are scattered at [Mutilprocess Model](../cor - Agent finds out the AgentWorkerLoader and then starts to load - use AgentWorkerLoader to load Worker synchronously in the sequence of Plugin Config, Extend, `agent.js` and other files. - The initiation of `agent.js` is able to be customized, and it supports asynchronous launch after which it notifies Master and invoke the function passed to `beforeStart`. -- After recieving the message that Agent Worker is launched,Master forks App Workers by cluster. - - App Workers are mutilple identical processes launched simultaneously - - App Worker is instantiated, which is similiar to Agent inherited Application class of framework loaded from framework path. +- After receiving the message that Agent Worker is launched,Master forks App Workers by cluster. + - App Workers are multiple identical processes launched simultaneously + - App Worker is instantiated, which is similar to Agent inherited Application class of framework loaded from framework path. - The same as Agent, Loading process of Application starts with AppWorkerLoader which loads files in the same order and finally informed Master. -- After informed of luanching successfully of each App Worker, Master is finally functioning. +- After informed of launching successfully of each App Worker, Master is finally functioning. ## Framework Testing -You'd better read [unittest](../core/unittest.md) first, which is similiar to framework testing in a quantity of situations. +You'd better read [unittest](../core/unittest.md) first, which is similar to framework testing in a quantity of situations. ### Initiation Here are some differences between initiation of frameworks. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/index.test.js', () => { let app; before(() => { @@ -293,15 +294,16 @@ describe('test/index.test.js', () => { - Different from application testing, framework testing tests framework code instead of application code, so that baseDir varies for the propose of testing kinds of applications. - BaseDir is potentially considered to be under the path of `test/fixtures`, otherwise it should be absolute paths. - The `framework` option is indispensable, which could be a absolute path or `true` meaning the path of the framework to be current directory. -- The use of the app should wait for the `ready` event in `before` hook, or some of the APIs is not avaiable. +- The use of the app should wait for the `ready` event in `before` hook, or some of the APIs is not available. - Do not forget to invoke `app.close()` after testing, which could arouse the exhausting of fds, caused by unclosed log files. ### Cache -`mm.app` enables cache as default, which means new envoriment setting would not work once loaded. +`mm.app` enables cache as default, which means new environment setting would not work once loaded. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('/test/index.test.js', () => { let app; afterEach(() => app.close()); @@ -327,15 +329,16 @@ describe('/test/index.test.js', () => { }); ``` -### Multipleprocess Testing +### Multiprocess Testing -Mutilprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model. +Multiprocess is rarely tested because of the high cost and the unavailability of API level's mock, meanwhile, processes have a slow start or even timeout, but it still remains the most effective way of testing multiprocess model. The option of `mock.cluster` have no difference with `mm.app` while their APIs are totally distinct, however, SuperTest still works. ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -346,16 +349,20 @@ describe('/test/index.test.js', () => { }); after(() => app.close()); afterEach(mock.restore); + it('should success', () => { - return app.httpRequest().get('/').expect(200); + return app.httpRequest() + .get('/') + .expect(200); }); }); ``` -Tests of `stdout/stderr` are also avaiable, since `mm.cluster` is based on [coffee](https://github.com/popomore/coffee) in which multiprocess testing is supported. +Tests of `stdout/stderr` are also available, since `mm.cluster` is based on [coffee](https://github.com/popomore/coffee) in which multiprocess testing is supported. ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('/test/index.test.js', () => { let app; before(() => { @@ -366,6 +373,7 @@ describe('/test/index.test.js', () => { return app.ready(); }); after(() => app.close()); + it('should get `started`', () => { // set the expectation of console app.expect('stdout', /started/); diff --git a/site/docs/advanced/framework.zh-CN.md b/site/docs/advanced/framework.zh-CN.md index 99dc71efac..69c957ccf0 100644 --- a/site/docs/advanced/framework.zh-CN.md +++ b/site/docs/advanced/framework.zh-CN.md @@ -259,6 +259,7 @@ AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader - 单个 App Worker 通过 framework 找到框架目录,实例化该框架的 Application 类。 - Application 根据 AppWorkerLoader 开始加载,加载顺序类似,会异步等待完成后通知 Master 启动完成。 4. Master 在等到所有 App Worker 发来的启动成功消息后,完成启动,开始对外提供服务。 + ## 框架测试 在看下文之前,请先查看[单元测试章节](../core/unittest.md)。框架测试的大部分使用场景和应用类似。 @@ -268,7 +269,8 @@ AgentWorkerLoader 的扩展也类似,这里不再赘述。AgentWorkerLoader 框架的初始化方式有一定差异。 ```js -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); + describe('test/index.test.js', () => { let app; before(() => { @@ -301,8 +303,9 @@ describe('test/index.test.js', () => { 在测试多环境场景需要使用到 cache 参数,因为 `mock.app` 默认有缓存,当第一次加载后再次加载会直接读取缓存,那么设置的环境也不会生效。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; afterEach(() => app.close()); @@ -334,8 +337,9 @@ describe('/test/index.test.js', () => { 多进程测试和 `mock.app` 参数一致,但 app 的 API 完全不同。不过,SuperTest 依然可用。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -346,8 +350,11 @@ describe('/test/index.test.js', () => { }); after(() => app.close()); afterEach(mock.restore); + it('should success', () => { - return app.httpRequest().get('/').expect(200); + return app.httpRequest() + .get('/') + .expect(200); }); }); ``` @@ -355,8 +362,9 @@ describe('/test/index.test.js', () => { 多进程测试还可以测试 stdout/stderr,因为 `mock.cluster` 是基于 [coffee](https://github.com/popomore/coffee) 扩展的,可进行进程测试。 ```js -const mock = require('egg-mock'); -describe('/test/index.test.js', () => { +const mock = require('@eggjs/mock'); + +describe('test/index.test.js', () => { let app; before(() => { app = mock.cluster({ @@ -366,6 +374,7 @@ describe('/test/index.test.js', () => { return app.ready(); }); after(() => app.close()); + it('should get `started`', () => { // 判断终端输出 app.expect('stdout', /started/); diff --git a/site/docs/basics/plugin.zh-CN.md b/site/docs/basics/plugin.zh-CN.md index d2aaa8b67d..5d7d75ea08 100644 --- a/site/docs/basics/plugin.zh-CN.md +++ b/site/docs/basics/plugin.zh-CN.md @@ -11,6 +11,7 @@ order: 9 - 如何编写一个插件? 接下来我们就来逐一讨论。 + ## 为什么要插件 我们在使用 Koa 中间件过程中发现了下面一些问题: @@ -35,12 +36,13 @@ order: 9 - 当遇到上一节提到的场景时,应用需引入插件。 - 插件本身可以包含中间件。 - 多个插件可以包装为一个[上层框架](../advanced/framework.md)。 + ## 使用插件 插件通常通过 npm 模块的方式进行复用: ```bash -$ npm i egg-mysql --save +npm i egg-mysql --save ``` **注意:我们推荐通过 `^` 的方式引入依赖,并且强烈不建议锁定版本。** @@ -118,7 +120,7 @@ exports.dev = { **注意:** -- `plugin.default.js` 不存在 +- `plugin.default.js` 不存在 - **只能在应用层使用,框架层请勿使用。** ### package 和 path @@ -135,6 +137,7 @@ exports.mysql = { path: path.join(__dirname, '../lib/plugin/egg-mysql'), }; ``` + ## 插件配置 插件一般会包含自己的默认配置。应用开发者可以在 `config.default.js` 中覆盖对应的配置: @@ -153,6 +156,7 @@ exports.mysql = { ``` 具体的合并规则可以参见[配置](./config.md)。 + ## 插件列表 - 框架默认内置了企业级应用[常用的插件](https://eggjs.org/zh-cn/plugins/): diff --git a/site/docs/basics/schedule.md b/site/docs/basics/schedule.md index 734f72bf9c..6e9b84d111 100644 --- a/site/docs/basics/schedule.md +++ b/site/docs/basics/schedule.md @@ -172,7 +172,7 @@ There are some scenarios we may need to manually execute scheduled tasks, for ex - Executing scheduled tasks manually for more elegant unit testing of scheduled tasks. ```js -const mm = require('egg-mock'); +const mm = require('@eggjs/mock'); const assert = require('assert'); it('should schedule work fine', async () => { diff --git a/site/docs/basics/schedule.zh-CN.md b/site/docs/basics/schedule.zh-CN.md index 40f9946bd1..0234ce3b4d 100644 --- a/site/docs/basics/schedule.zh-CN.md +++ b/site/docs/basics/schedule.zh-CN.md @@ -170,7 +170,7 @@ module.exports = (app) => { - 手动执行定时任务可以更优雅地编写定时任务的单元测试。 ```js -const mm = require('egg-mock'); +const mm = require('@eggjs/mock'); const assert = require('assert'); it('should schedule work fine', async () => { @@ -218,4 +218,4 @@ module.exports = (agent) => { - `this.schedule` - 定时任务的属性,所有任务默认支持的 `disable` 属性,以及其他自定义配置的解析。 - `this.sendOne(...args)` - 随机通知某个 worker 执行 task,`args` 会传递给 `subscribe(...args)` 或 `task(ctx, ...args)` 方法。 -- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 \ No newline at end of file +- `this.sendAll(...args)` - 通知所有的 worker 执行 task。 diff --git a/site/docs/core/unittest.md b/site/docs/core/unittest.md index 6ee85db358..aeaff19ca9 100644 --- a/site/docs/core/unittest.md +++ b/site/docs/core/unittest.md @@ -136,7 +136,7 @@ We can easily create an app instance with Mocha's `before` hook through egg-mock ```js // test/controller/home.test.js const assert = require('assert'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/controller/home.test.js', () => { let app; diff --git a/site/docs/core/unittest.zh-CN.md b/site/docs/core/unittest.zh-CN.md index 70c985ec9d..881885ddad 100644 --- a/site/docs/core/unittest.zh-CN.md +++ b/site/docs/core/unittest.zh-CN.md @@ -139,7 +139,7 @@ npm test ```javascript // test/controller/home.test.js const assert = require('assert'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); describe('test/controller/home.test.js', () => { let app; diff --git a/src/agent.ts b/src/agent.ts new file mode 100644 index 0000000000..0f64b51da1 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,7 @@ +import { BaseHookClass } from './lib/core/base_hook_class.js'; + +export default class EggAgentHook extends BaseHookClass { + configDidLoad() { + this.agent._wrapMessenger(); + } +} diff --git a/src/app/extend/context.ts b/src/app/extend/context.ts new file mode 100644 index 0000000000..ff3a60eead --- /dev/null +++ b/src/app/extend/context.ts @@ -0,0 +1,302 @@ +import delegate from 'delegates'; +import { assign } from 'utility'; +import { now, diff } from 'performance-ms'; +import { + utils, Context as EggCoreContext, Router, + type ContextDelegation as EggCoreContextDelegation, +} from '@eggjs/core'; +import type { Cookies as ContextCookies } from '@eggjs/cookies'; +import { EggLogger } from 'egg-logger'; +import type { Application } from '../../lib/application.js'; +import type { + HttpClientRequestURL, HttpClientRequestOptions, HttpClient, +} from '../../lib/core/httpclient.js'; +import type { BaseContextClass } from '../../lib//core/base_context_class.js'; +import Request from './request.js'; +import Response from './response.js'; +import type Helper from './helper.js'; + +import './context.types.js'; + +const HELPER = Symbol('ctx helper'); +const LOCALS = Symbol('ctx locals'); +const LOCALS_LIST = Symbol('ctx localsList'); +const COOKIES = Symbol('ctx cookies'); +const CONTEXT_HTTPCLIENT = Symbol('ctx httpclient'); +const CONTEXT_ROUTER = Symbol('ctx router'); + +interface Cookies extends ContextCookies { + request: any; + response: any; +} + +export default class Context extends EggCoreContext { + declare app: Application; + declare request: Request; + declare service: BaseContextClass; + + /** + * Request start time + * @member {Number} Context#starttime + */ + starttime: number; + /** + * Request start timer using `performance.now()` + * @member {Number} Context#performanceStarttime + */ + performanceStarttime: number; + + /** + * Get the current visitor's cookies. + */ + get cookies() { + let cookies = this[COOKIES]; + if (!cookies) { + this[COOKIES] = cookies = new this.app.ContextCookies(this, this.app.keys, this.app.config.cookies); + } + return cookies as Cookies; + } + + /** + * Get a wrapper httpclient instance contain ctx in the hold request process + * + * @return {HttpClient} the wrapper httpclient instance + */ + get httpclient(): HttpClient { + if (!this[CONTEXT_HTTPCLIENT]) { + this[CONTEXT_HTTPCLIENT] = new this.app.ContextHttpClient(this as any); + } + return this[CONTEXT_HTTPCLIENT] as HttpClient; + } + + /** + * Alias to {@link Context#httpclient} + */ + get httpClient(): HttpClient { + return this.httpclient; + } + + /** + * Shortcut for httpclient.curl + * + * @function Context#curl + * @param {String|Object} url - request url address. + * @param {Object} [options] - options for request. + * @return {Object} see {@link ContextHttpClient#curl} + */ + async curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions): ReturnType { + return await this.httpclient.curl(url, options); + } + + /** + * Alias to {@link Application#router} + * + * @member {Router} Context#router + * @since 1.0.0 + * @example + * ```js + * this.router.pathFor('post', { id: 12 }); + * ``` + */ + get router(): Router { + if (this[CONTEXT_ROUTER]) { + return this[CONTEXT_ROUTER] as Router; + } + return this.app.router; + } + + /** + * Set router to Context, only use on EggRouter + * @param {Router} val router instance + */ + set router(val: Router) { + this[CONTEXT_ROUTER] = val; + } + + /** + * Get helper instance from {@link Application#Helper} + * + * @member {Helper} Context#helper + * @since 1.0.0 + */ + get helper(): Helper { + if (!this[HELPER]) { + this[HELPER] = new this.app.Helper(this as any); + } + return this[HELPER] as Helper; + } + + /** + * Wrap app.loggers with context information, + * if a custom logger is defined by naming aLogger, then you can `ctx.getLogger('aLogger')` + * + * @param {String} name - logger name + */ + getLogger(name: string): EggLogger { + return this.app.getLogger(name); + } + + /** + * Logger for Application + * + * @member {Logger} Context#logger + * @since 1.0.0 + * @example + * ```js + * this.logger.info('some request data: %j', this.request.body); + * this.logger.warn('WARNING!!!!'); + * ``` + */ + get logger(): EggLogger { + return this.getLogger('logger'); + } + + /** + * Logger for frameworks and plugins + * + * @member {Logger} Context#coreLogger + * @since 1.0.0 + */ + get coreLogger(): EggLogger { + return this.getLogger('coreLogger'); + } + + /** + * locals is an object for view, you can use `app.locals` and `ctx.locals` to set variables, + * which will be used as data when view is rendering. + * The difference between `app.locals` and `ctx.locals` is the context level, `app.locals` is global level, and `ctx.locals` is request level. when you get `ctx.locals`, it will merge `app.locals`. + * + * when you set locals, only object is available + * + * ```js + * this.locals = { + * a: 1 + * }; + * this.locals = { + * b: 1 + * }; + * this.locals.c = 1; + * console.log(this.locals); + * { + * a: 1, + * b: 1, + * c: 1, + * }; + * ``` + * + * `ctx.locals` has cache, it only merges `app.locals` once in one request. + * + * @member {Object} Context#locals + */ + get locals() { + if (!this[LOCALS]) { + this[LOCALS] = assign({}, this.app.locals); + } + if (Array.isArray(this[LOCALS_LIST]) && this[LOCALS_LIST].length > 0) { + assign(this[LOCALS], this[LOCALS_LIST]); + this[LOCALS_LIST] = null; + } + return this[LOCALS] as Record; + } + + set locals(val) { + const localsList = this[LOCALS_LIST] as Record[] ?? []; + localsList.push(val); + this[LOCALS_LIST] = localsList; + } + + /** + * alias to {@link Context#locals}, compatible with koa that use this variable + * @member {Object} state + * @see Context#locals + */ + get state() { + return this.locals; + } + + set state(val) { + this.locals = val; + } + + /** + * Run async function in the background + * @param {Function} scope - the first args is ctx + * ```js + * this.body = 'hi'; + * + * this.runInBackground(async ctx => { + * await ctx.mysql.query(sql); + * await ctx.curl(url); + * }); + * ``` + */ + runInBackground(scope: (ctx: ContextDelegation) => Promise, taskName?: string): void { + // try to use custom function name first + if (!taskName) { + taskName = Reflect.get(scope, '_name') || scope.name || utils.getCalleeFromStack(true); + } + // use setImmediate to ensure all sync logic will run async + setImmediate(() => { + this._runInBackground(scope, taskName!); + }); + } + + // let plugins or frameworks to reuse _runInBackground in some cases. + // e.g.: https://github.com/eggjs/egg-mock/pull/78 + async _runInBackground(scope: (ctx: ContextDelegation) => Promise, taskName: string) { + const startTime = now(); + try { + await scope(this as any); + this.coreLogger.info('[egg:background] task:%s success (%dms)', taskName, diff(startTime)); + } catch (err: any) { + // background task process log + this.coreLogger.info('[egg:background] task:%s fail (%dms)', taskName, diff(startTime)); + + // emit error when promise catch, and set err.runInBackground flag + err.runInBackground = true; + this.app.emit('error', err, this); + } + } +} + +/** + * Context delegation. + */ + +delegate(Context.prototype, 'request') + /** + * @member {Boolean} Context#acceptJSON + * @see Request#acceptJSON + * @since 1.0.0 + */ + .getter('acceptJSON') + /** + * @member {Array} Context#queries + * @see Request#queries + * @since 1.0.0 + */ + .getter('queries') + /** + * @member {Boolean} Context#accept + * @see Request#accept + * @since 1.0.0 + */ + .getter('accept') + /** + * @member {string} Context#ip + * @see Request#ip + * @since 1.0.0 + */ + .access('ip'); + +delegate(Context.prototype, 'response') + /** + * @member {Number} Context#realStatus + * @see Response#realStatus + * @since 1.0.0 + */ + .access('realStatus'); + +export type ContextDelegation = EggCoreContextDelegation & Context +& Pick +& Pick; diff --git a/src/app/extend/context.types.ts b/src/app/extend/context.types.ts new file mode 100644 index 0000000000..0c849085b0 --- /dev/null +++ b/src/app/extend/context.types.ts @@ -0,0 +1,21 @@ +import type { + Router, +} from '@eggjs/core'; +import type { + HttpClientRequestURL, HttpClientRequestOptions, HttpClient, +} from '../../lib/core/httpclient.js'; +import type Helper from './helper.js'; +import type { EggLogger } from 'egg-logger'; + +declare module '@eggjs/core' { + // add Context overrides types + interface Context { + curl(url: HttpClientRequestURL, options?: HttpClientRequestOptions): ReturnType; + get router(): Router; + set router(val: Router); + get helper(): Helper; + get httpclient(): HttpClient; + get httpClient(): HttpClient; + getLogger(name: string): EggLogger; + } +} diff --git a/app/extend/helper.js b/src/app/extend/helper.ts similarity index 64% rename from app/extend/helper.js rename to src/app/extend/helper.ts index 3120944d25..56a40e0d16 100644 --- a/app/extend/helper.js +++ b/src/app/extend/helper.ts @@ -1,10 +1,12 @@ -'use strict'; - -const url = require('url'); - - -module.exports = { +import url from 'node:url'; +import { BaseContextClass } from '../../lib/core/base_context_class.js'; +/** + * 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. + */ +export default class Helper extends BaseContextClass { /** * Generate URL path(without host) for route. Takes the route name and a map of named params. * @function Helper#pathFor @@ -19,9 +21,9 @@ module.exports = { * ``` * @return {String} url path(without host) */ - pathFor(name, params) { + pathFor(name: string, params: Record): string { return this.app.router.url(name, params); - }, + } /** * Generate full URL(with host) for route. Takes the route name and a map of named params. @@ -36,8 +38,7 @@ module.exports = { * ``` * @return {String} full url(with host) */ - urlFor(name, params) { - return this.ctx.protocol + '://' + this.ctx.host + url.resolve('/', this.app.router.url(name, params)); - }, - -}; + urlFor(name: string, params: Record): string { + return this.ctx.protocol + '://' + this.ctx.host + url.resolve('/', this.pathFor(name, params)); + } +} diff --git a/app/extend/request.js b/src/app/extend/request.ts similarity index 62% rename from app/extend/request.js rename to src/app/extend/request.ts index 7dccdd353a..a60edfb385 100644 --- a/app/extend/request.js +++ b/src/app/extend/request.ts @@ -1,17 +1,21 @@ -'use strict'; +import querystring from 'node:querystring'; +import { Request as EggCoreRequest } from '@eggjs/core'; +import type { Application } from '../../lib/application.js'; +import type { ContextDelegation } from './context.js'; +import Response from './response.js'; -const querystring = require('querystring'); -const accepts = require('accepts'); - -const _querycache = Symbol('_querycache'); -const _queriesCache = Symbol('_queriesCache'); -const PROTOCOL = Symbol('PROTOCOL'); -const HOST = Symbol('HOST'); -const ACCEPTS = Symbol('ACCEPTS'); -const IPS = Symbol('IPS'); +const QUERY_CACHE = Symbol('request query cache'); +const QUERIES_CACHE = Symbol('request queries cache'); +const PROTOCOL = Symbol('request protocol'); +const HOST = Symbol('request host'); +const IPS = Symbol('request ips'); const RE_ARRAY_KEY = /[^\[\]]+\[\]$/; -module.exports = { +export default class Request extends EggCoreRequest { + declare app: Application; + declare ctx: ContextDelegation; + declare response: Response; + /** * Parse the "Host" header field host * and support X-Forwarded-Host when a @@ -29,17 +33,19 @@ module.exports = { * => 'demo.eggjs.org' * ``` */ - get host() { - if (this[HOST]) return this[HOST]; + get host(): string { + let host = this[HOST] as string | undefined; + if (host) { + return host; + } - let host; if (this.app.config.proxy) { host = getFromHeaders(this, this.app.config.hostHeaders); } host = host || this.get('host') || ''; - this[HOST] = host.split(/\s*,\s*/)[0]; - return this[HOST]; - }, + this[HOST] = host = host.split(',')[0].trim(); + return host; + } /** * @member {String} Request#protocol @@ -49,25 +55,28 @@ module.exports = { * => 'https' * ``` */ - get protocol() { - if (this[PROTOCOL]) return this[PROTOCOL]; + get protocol(): string { + let protocol = this[PROTOCOL] as string; + if (protocol) { + return protocol; + } // detect encrypted socket - if (this.socket && this.socket.encrypted) { - this[PROTOCOL] = 'https'; - return this[PROTOCOL]; + if (this.socket?.encrypted) { + this[PROTOCOL] = protocol = 'https'; + return protocol; } // get from headers specified in `app.config.protocolHeaders` if (this.app.config.proxy) { const proto = getFromHeaders(this, this.app.config.protocolHeaders); if (proto) { - this[PROTOCOL] = proto.split(/\s*,\s*/)[0]; - return this[PROTOCOL]; + this[PROTOCOL] = protocol = proto.split(/\s*,\s*/)[0]; + return protocol; } } - // use protocol specified in `app.conig.protocol` - this[PROTOCOL] = this.app.config.protocol || 'http'; - return this[PROTOCOL]; - }, + // use protocol specified in `app.config.protocol` + this[PROTOCOL] = protocol = this.app.config.protocol || 'http'; + return protocol; + } /** * Get all pass through ip addresses from the request. @@ -80,29 +89,34 @@ module.exports = { * => ['100.23.1.2', '201.10.10.2'] * ``` */ - get ips() { - if (this[IPS]) return this[IPS]; + get ips(): string[] { + let ips = this[IPS] as string[] | undefined; + if (ips) { + return ips; + } // return empty array when proxy=false if (!this.app.config.proxy) { - this[IPS] = []; - return this[IPS]; + this[IPS] = ips = []; + return ips; } - const val = getFromHeaders(this, this.app.config.ipHeaders) || ''; - this[IPS] = val ? val.split(/\s*,\s*/) : []; + const val = getFromHeaders(this, this.app.config.ipHeaders); + this[IPS] = ips = val ? val.split(/\s*,\s*/) : []; let maxIpsCount = this.app.config.maxIpsCount; // Compatible with maxProxyCount logic (previous logic is wrong, only for compatibility with legacy logic) - if (!maxIpsCount && this.app.config.maxProxyCount) maxIpsCount = this.app.config.maxProxyCount + 1; + if (!maxIpsCount && this.app.config.maxProxyCount) { + maxIpsCount = this.app.config.maxProxyCount + 1; + } if (maxIpsCount > 0) { // if maxIpsCount present, only keep `maxIpsCount` ips // [ illegalIp, clientRealIp, proxyIp1, proxyIp2 ...] - this[IPS] = this[IPS].slice(-maxIpsCount); + this[IPS] = ips = ips.slice(-maxIpsCount); } - return this[IPS]; - }, + return ips; + } /** * Get the request remote IPv4 address @@ -115,16 +129,16 @@ module.exports = { * => '111.10.2.1' * ``` */ - get ip() { + get ip(): string { if (this._ip) { return this._ip; } - const ip = this.ips[0] || this.socket.remoteAddress; + const ip = this.ips[0] ?? this.socket.remoteAddress; // will be '::ffff:x.x.x.x', should convert to standard IPv4 format // https://zh.wikipedia.org/wiki/IPv6 - this._ip = ip && ip.indexOf('::ffff:') > -1 ? ip.substring(7) : ip; + this._ip = ip && ip.startsWith('::ffff:') ? ip.substring(7) : ip; return this._ip; - }, + } /** * Set the request remote IPv4 address @@ -137,9 +151,9 @@ module.exports = { * => '111.10.2.1' * ``` */ - set ip(ip) { + set ip(ip: string) { this._ip = ip; - }, + } /** * detect if response should be json @@ -150,25 +164,25 @@ module.exports = { * @member {Boolean} Request#acceptJSON * @since 1.0.0 */ - get acceptJSON() { + get acceptJSON(): boolean { if (this.path.endsWith('.json')) return true; if (this.response.type && this.response.type.indexOf('json') >= 0) return true; if (this.accepts('html', 'text', 'json') === 'json') return true; return false; - }, + } // How to read query safely // https://github.com/koajs/qs/issues/5 - _customQuery(cacheName, filter) { + _customQuery(cacheName: symbol, filter: (value: string | string[]) => string | string[]) { const str = this.querystring || ''; - let c = this[cacheName]; + let c = this[cacheName] as Record>; if (!c) { c = this[cacheName] = {}; } let cacheQuery = c[str]; if (!cacheQuery) { cacheQuery = c[str] = {}; - const isQueries = cacheName === _queriesCache; + const isQueries = cacheName === QUERIES_CACHE; // `querystring.parse` CANNOT parse something like `a[foo]=1&a[bar]=2` const query = str ? querystring.parse(str) : {}; for (const key in query) { @@ -176,20 +190,19 @@ module.exports = { // key is '', like `a=b&` continue; } - const value = filter(query[key]); + const value = filter(query[key]!); cacheQuery[key] = value; if (isQueries && RE_ARRAY_KEY.test(key)) { // `this.queries['key'] => this.queries['key[]']` is compatibly supported - const subkey = key.substring(0, key.length - 2); - - if (!cacheQuery[subkey]) { - cacheQuery[subkey] = value; + const subKey = key.substring(0, key.length - 2); + if (!cacheQuery[subKey]) { + cacheQuery[subKey] = value; } } } } return cacheQuery; - }, + } /** * get params pass by querystring, all values are of string type. @@ -212,8 +225,8 @@ module.exports = { * ``` */ get query() { - return this._customQuery(_querycache, firstValue); - }, + return this._customQuery(QUERY_CACHE, firstValue) as Record; + } /** * get params pass by querystring, all value are Array type. {@link Request#query} @@ -232,50 +245,39 @@ module.exports = { * ``` */ get queries() { - return this._customQuery(_queriesCache, arrayValue); - }, - - get accept() { - let accept = this[ACCEPTS]; - if (accept) { - return accept; - } - accept = this[ACCEPTS] = accepts(this.req); - return accept; - }, + return this._customQuery(QUERIES_CACHE, arrayValue) as Record; + } /** * Set query-string as an object. * * @function Request#query * @param {Object} obj set querystring and query object for request. - * @return {void} */ - set query(obj) { + set query(obj: Record) { this.querystring = querystring.stringify(obj); - }, -}; - + } +} -function firstValue(value) { +function firstValue(value: string | string[]) { if (Array.isArray(value)) { value = value[0]; } return value; } -function arrayValue(value) { +function arrayValue(value: string | string[]) { if (!Array.isArray(value)) { value = [ value ]; } return value; } -function getFromHeaders(ctx, names) { +function getFromHeaders(request: Request, names: string) { if (!names) return ''; - names = names.split(/\s*,\s*/); - for (const name of names) { - const value = ctx.get(name); + const fields = names.split(/\s*,\s*/); + for (const name of fields) { + const value = request.get(name); if (value) return value; } return ''; diff --git a/src/app/extend/response.ts b/src/app/extend/response.ts new file mode 100644 index 0000000000..3611a129b6 --- /dev/null +++ b/src/app/extend/response.ts @@ -0,0 +1,36 @@ +import { Response as KoaResponse } from '@eggjs/core'; + +const REAL_STATUS = Symbol('response realStatus'); + +export default class Response extends KoaResponse { + /** + * Get or set a real status code. + * + * e.g.: Using 302 status redirect to the global error page + * instead of show current 500 status page. + * And access log should save 500 not 302, + * then the `realStatus` can help us find out the real status code. + * @member {Number} Response#realStatus + * @return {Number} The status code to be set. + */ + get realStatus(): number { + if (this[REAL_STATUS]) { + return this[REAL_STATUS] as number; + } + return this.status; + } + + /** + * Set a real status code. + * + * e.g.: Using 302 status redirect to the global error page + * instead of show current 500 status page. + * And access log should save 500 not 302, + * then the `realStatus` can help us find out the real status code. + * @member {Number} Response#realStatus + * @param {Number} status The status code to be set. + */ + set realStatus(status: number) { + this[REAL_STATUS] = status; + } +} 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 53% rename from app/middleware/meta.js rename to src/app/middleware/meta.ts index 9e5283f53c..97540bd8ce 100644 --- a/app/middleware/meta.js +++ b/src/app/middleware/meta.ts @@ -2,12 +2,19 @@ * meta middleware, should be the first middleware */ -const { performance } = require('perf_hooks'); +import { performance } from 'node:perf_hooks'; +import type { ContextDelegation, Next } from '../../lib/egg.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: ContextDelegation, 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 72% rename from app/middleware/notfound.js rename to src/app/middleware/notfound.ts index 4ec4288f8e..2e4ea31124 100644 --- a/app/middleware/notfound.js +++ b/src/app/middleware/notfound.ts @@ -1,7 +1,12 @@ -'use strict'; +import type { Next, ContextDelegation } from '../../lib/egg.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: ContextDelegation, 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..b6ed15c466 --- /dev/null +++ b/src/app/middleware/site_file.ts @@ -0,0 +1,68 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { readFile } from 'node:fs/promises'; +import type { Next, ContextDelegation } from '../../lib/egg.js'; + +export type SiteFileContentFun = (ctx: ContextDelegation) => Promise; + +export interface SiteFileMiddlewareOptions { + enable: boolean; + cacheControl: string; + [key: string]: string | Buffer | boolean | SiteFileContentFun | URL; +} + +const BUFFER_CACHE = Symbol('siteFile URL buffer cache'); + +export default (options: SiteFileMiddlewareOptions) => { + return async function siteFile(ctx: ContextDelegation, next: Next) { + if (ctx.method !== 'HEAD' && ctx.method !== 'GET') { + return next(); + } + 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); + } + + // URL + if (content instanceof URL) { + if (content.protocol !== 'file:') { + return ctx.redirect(content.href); + } + // protocol = file: + let buffer = Reflect.get(content, BUFFER_CACHE) as Buffer; + if (!buffer) { + buffer = await readFile(fileURLToPath(content)); + Reflect.set(content, BUFFER_CACHE, buffer); + } + ctx.set('cache-control', options.cacheControl); + ctx.body = content; + ctx.type = path.extname(ctx.path); + return; + } + + // '/robots.txt': Buffer { - - const config = { - +export default (appInfo: EggAppInfo) => { + const config: Partial = { /** * The environment of egg * @member {String} Config#env @@ -131,7 +129,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 +151,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 +174,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,13 +183,14 @@ module.exports = appInfo => { * @property {String} pageUrl - the 404 page url */ config.notfound = { + enable: true, pageUrl: '', }; /** * The option of `siteFile` middleware * - * You can map some files using this options, it will response immdiately when matching. + * You can map some files using this options, it will response immediately when matching. * * @member {Object} Config#siteFile - key is path, and value is url or buffer. * @property {String} cacheControl - files cache , default is public, max-age=2592000 @@ -202,13 +201,14 @@ module.exports = appInfo => { * }; */ config.siteFile = { - '/favicon.ico': fs.readFileSync(path.join(__dirname, 'favicon.png')), + enable: true, + '/favicon.ico': pathToFileURL(getSourceFile('config/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 +236,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 +265,7 @@ 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} enableFastContextLogger - using the app logger instead of EggContextLogger, defaults to true */ config.logger = { dir: path.join(appInfo.root, 'logs', appInfo.name), @@ -282,8 +282,7 @@ module.exports = appInfo => { errorLogName: 'common-error.log', coreLogger: {}, allowDebugAtProd: false, - enablePerformanceTimer: false, - enableFastContextLogger: false, + enableFastContextLogger: true, }; /** @@ -306,34 +305,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 +351,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 +398,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..6ba0294b1a --- /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..3c16597396 --- /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 96% rename from config/plugin.js rename to src/config/plugin.ts index da864347eb..5c22a02e01 100644 --- a/config/plugin.js +++ b/src/config/plugin.ts @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export default { // enable plugins /** @@ -43,7 +41,7 @@ module.exports = { */ watcher: { enable: true, - package: 'egg-watcher', + package: '@eggjs/watcher', }, /** @@ -98,7 +96,7 @@ module.exports = { */ schedule: { enable: true, - package: 'egg-schedule', + package: '@eggjs/schedule', }, /** diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000000..64b3df5088 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,80 @@ +/** + * @namespace Egg + */ + +import { BaseContextClass } from './lib/core/base_context_class.js'; +import { startEgg } from './lib/start.js'; +import Helper from './app/extend/helper.js'; + +// export extends +export { Helper }; + +// export types +export * from './lib/egg.js'; +export * from './lib/type.js'; +export * from './lib/start.js'; + +/** + * Start egg application with cluster mode + * @since 1.0.0 + */ +export * from '@eggjs/cluster'; + +/** + * Start egg application with single process mode + * @since 1.0.0 + */ +export const start = startEgg; + +/** + * @member {Application} Egg#Application + * @since 1.0.0 + */ +export { Application } from './lib/application.js'; + +/** + * @member {Agent} Egg#Agent + * @since 1.0.0 + */ +export { Agent } from './lib/agent.js'; + +/** + * @member {AppWorkerLoader} Egg#AppWorkerLoader + * @since 1.0.0 + */ + +/** + * @member {AgentWorkerLoader} Egg#AgentWorkerLoader + * @since 1.0.0 + */ + +export { AppWorkerLoader, AgentWorkerLoader } from './lib/loader/index.js'; + +/** + * @member {Controller} Egg#Controller + * @since 1.1.0 + */ +export const Controller = BaseContextClass; + +/** + * @member {Service} Egg#Service + * @since 1.1.0 + */ +export const Service = BaseContextClass; + +/** + * @member {Subscription} Egg#Subscription + * @since 1.10.0 + */ +export const Subscription = BaseContextClass; + +/** + * @member {BaseContextClass} Egg#BaseContextClass + * @since 1.2.0 + */ +export { BaseContextClass } from './lib/core/base_context_class.js'; + +/** + * @member {Boot} Egg#Boot + */ +export { BaseHookClass as Boot } from './lib/core/base_hook_class.js'; diff --git a/src/lib/agent.ts b/src/lib/agent.ts new file mode 100644 index 0000000000..49731018fe --- /dev/null +++ b/src/lib/agent.ts @@ -0,0 +1,66 @@ +import { EggLogger } from 'egg-logger'; +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 EggApplicationCore} + * @augments EggApplicationCore + */ +export class Agent extends EggApplicationCore { + readonly #agentAliveHandler: NodeJS.Timeout; + + /** + * @class + * @param {Object} options - see {@link EggApplicationCore} + */ + constructor(options?: Omit) { + super({ + ...options, + type: 'agent', + }); + + // keep agent alive even it doesn't have any io tasks + this.#agentAliveHandler = setInterval(() => { + this.coreLogger.info('[]'); + }, 24 * 60 * 60 * 1000); + } + + get [EGG_LOADER]() { + return AgentWorkerLoader; + } + + _wrapMessenger() { + for (const methodName of [ + 'broadcast', + 'sendTo', + 'sendToApp', + 'sendToAgent', + 'sendRandom', + ]) { + wrapMethod(methodName, this.messenger, this.coreLogger); + } + + function wrapMethod(methodName: string, messenger: any, logger: EggLogger) { + const originMethod = messenger[methodName]; + 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, + ); + originMethod.apply(this, args); + }; + messenger.prependOnceListener('egg-ready', () => { + messenger[methodName] = originMethod; + }); + } + } + + async close() { + clearInterval(this.#agentAliveHandler); + await super.close(); + } +} diff --git a/lib/application.js b/src/lib/application.ts similarity index 61% rename from lib/application.js rename to src/lib/application.ts index eac56c9c0a..1d38d8fb9b 100644 --- a/lib/application.js +++ b/src/lib/application.ts @@ -1,27 +1,19 @@ -'use strict'; +import path from 'node:path'; +import fs from 'node:fs'; +import http from 'node:http'; +import { Socket } from 'node:net'; +import { graceful } from 'graceful'; +import { assign } from 'utility'; +import { utils as eggUtils } from '@eggjs/core'; +import { + EggApplicationCore, + type EggApplicationCoreOptions, + type ContextDelegation, +} from './egg.js'; +import { AppWorkerLoader } from './loader/index.js'; +import Helper from '../app/extend/helper.js'; -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'); -const eggUtils = require('egg-core').utils; -const EggApplication = require('./egg'); -const AppWorkerLoader = require('./loader').AppWorkerLoader; - -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 EGG_PATH = Symbol.for('egg#eggPath'); -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 @@ -38,59 +30,52 @@ 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 /** - * Singleton instance in App Worker, extend {@link EggApplication} - * @augments EggApplication + * Singleton instance in App Worker, extend {@link EggApplicationCore} + * @augments EggApplicationCore */ -class Application extends EggApplication { +export class Application extends EggApplicationCore { + // will auto set after 'server' event emit + server?: http.Server; + #locals: Record = {}; + /** + * reference to {@link Helper} + * @member {Helper} Application#Helper + */ + Helper = Helper; /** * @class - * @param {Object} options - see {@link EggApplication} + * @param {Object} options - see {@link EggApplicationCore} */ - 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; } - get [EGG_PATH]() { - return path.join(__dirname, '..'); - } - - [RESPONSE_RAW](socket, raw) { - /* istanbul ignore next */ - if (!socket.writable) return; - if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + #responseRaw(socket: Socket, raw?: any) { + if (!socket?.writable) return; + if (!raw) { + return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + } const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body; const headers = raw.headers || {}; @@ -114,10 +99,10 @@ 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, @@ -143,35 +128,34 @@ 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 const serverGracefulIgnoreCode = this.config.serverGracefulIgnoreCode || []; - /* 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, @@ -182,10 +166,12 @@ 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); + } } /** @@ -194,24 +180,11 @@ class Application extends EggApplication { * @see Context#locals */ get locals() { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - return this[LOCALS]; + return this.#locals; } - set locals(val) { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - - assign(this[LOCALS], val); - } - - 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); } /** @@ -234,11 +207,11 @@ 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}`); } } @@ -248,11 +221,13 @@ class Application extends EggApplication { * @see Context#runInBackground * @param {Function} scope - the first args is an anonymous ctx */ - runInBackground(scope) { - const ctx = this.createAnonymousContext(); - if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + runInBackground(scope: (ctx: ContextDelegation) => Promise, req?: unknown) { + const ctx = this.createAnonymousContext(req); + if (!scope.name) { + Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); + } this.ctxStorage.run(ctx, () => { - ctx.runInBackground(scope); + return ctx.runInBackground(scope); }); } @@ -262,9 +237,11 @@ 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: ContextDelegation) => 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); }); @@ -275,7 +252,7 @@ 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'); @@ -284,31 +261,9 @@ 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; } /** @@ -316,17 +271,15 @@ 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)); } /** @@ -334,15 +287,14 @@ 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]); } }); } } -module.exports = Application; diff --git a/src/lib/core/base_context_class.ts b/src/lib/core/base_context_class.ts new file mode 100644 index 0000000000..9ece0b1152 --- /dev/null +++ b/src/lib/core/base_context_class.ts @@ -0,0 +1,21 @@ +import { BaseContextClass as EggCoreBaseContextClass } from '@eggjs/core'; +import type { ContextDelegation } from '../egg.js'; +import { BaseContextLogger } from './base_context_logger.js'; + +/** + * BaseContextClass is a base class that can be extended, + * it's instantiated in context level, + * {@link Helper}, {@link Service} is extending it. + */ +export class BaseContextClass extends EggCoreBaseContextClass { + declare ctx: ContextDelegation; + protected pathName?: string; + #logger?: BaseContextLogger; + + get 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 new file mode 100644 index 0000000000..eba0ac32c4 --- /dev/null +++ b/src/lib/core/base_context_logger.ts @@ -0,0 +1,67 @@ +import type { EggContext } from '../egg.js'; + +export class BaseContextLogger { + readonly #ctx: EggContext; + readonly #pathName?: string; + + /** + * @class + * @param {Context} ctx - context instance + * @param {String} pathName - class path name + * @since 1.0.0 + */ + constructor(ctx: EggContext, pathName?: string) { + /** + * @member {Context} BaseContextLogger#ctx + * @since 1.2.0 + */ + this.#ctx = ctx; + this.#pathName = pathName; + } + + 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); + } + + /** + * @member {Function} BaseContextLogger#debug + * @param {...any} args - log msg + * @since 1.2.0 + */ + debug(...args: any[]) { + this._log('debug', args); + } + + /** + * @member {Function} BaseContextLogger#info + * @param {...any} args - log msg + * @since 1.2.0 + */ + info(...args: any[]) { + this._log('info', args); + } + + /** + * @member {Function} BaseContextLogger#warn + * @param {...any} args - log msg + * @since 1.2.0 + */ + warn(...args: any[]) { + this._log('warn', args); + } + + /** + * @member {Function} BaseContextLogger#error + * @param {...any} args - log msg + * @since 1.2.0 + */ + error(...args: any[]) { + this._log('error', args); + } +} diff --git a/src/lib/core/base_hook_class.ts b/src/lib/core/base_hook_class.ts new file mode 100644 index 0000000000..2e718a73da --- /dev/null +++ b/src/lib/core/base_hook_class.ts @@ -0,0 +1,30 @@ +import assert from 'node:assert'; +import type { ILifecycleBoot } from '@eggjs/core'; +import type { Application, Agent } from '../../index.js'; + +export class BaseHookClass implements ILifecycleBoot { + fullPath?: string; + #instance: Application | Agent; + + constructor(instance: Application | Agent) { + this.#instance = instance; + } + + get logger(): any { + return this.#instance.logger; + } + + get config() { + return this.#instance.config; + } + + get app() { + 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 as Agent; + } +} diff --git a/src/lib/core/context_httpclient.ts b/src/lib/core/context_httpclient.ts new file mode 100644 index 0000000000..fa2ba1a793 --- /dev/null +++ b/src/lib/core/context_httpclient.ts @@ -0,0 +1,33 @@ +import type { ContextDelegation, EggApplicationCore } from '../egg.js'; +import type { + HttpClientRequestURL, HttpClientRequestOptions, +} from './httpclient.js'; + +export class ContextHttpClient { + ctx: ContextDelegation; + app: EggApplicationCore; + + constructor(ctx: ContextDelegation) { + 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 new file mode 100644 index 0000000000..38f7cf6e3f --- /dev/null +++ b/src/lib/core/httpclient.ts @@ -0,0 +1,51 @@ +import { + HttpClient as RawHttpClient, + RequestURL as HttpClientRequestURL, + RequestOptions, +} from 'urllib'; +import { ms } from 'humanize-ms'; +import type { EggApplicationCore, ContextDelegation } from '../egg.js'; + +export type { + HttpClientResponse, + RequestURL as HttpClientRequestURL, +} from 'urllib'; + +export interface HttpClientRequestOptions extends RequestOptions { + ctx?: ContextDelegation; + tracer?: unknown; +} + +export class HttpClient extends RawHttpClient { + readonly #app: EggApplicationCore & { tracer?: unknown }; + + constructor(app: EggApplicationCore) { + normalizeConfig(app); + const config = app.config.httpclient; + super({ + defaultArgs: config.request, + }); + this.#app = app; + } + + async request(url: HttpClientRequestURL, 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: HttpClientRequestURL, options?: HttpClientRequestOptions) { + return await this.request(url, options); + } +} + +function normalizeConfig(app: EggApplicationCore) { + const config = app.config.httpclient; + if (typeof config.request?.timeout === 'string') { + config.request.timeout = ms(config.request.timeout); + } +} diff --git a/src/lib/core/logger.ts b/src/lib/core/logger.ts new file mode 100644 index 0000000000..e9c4dcbd7b --- /dev/null +++ b/src/lib/core/logger.ts @@ -0,0 +1,42 @@ +import { EggLoggers, EggLoggersOptions } from 'egg-logger'; +import { setCustomLogger } from 'onelogger'; +import type { EggApplicationCore } from '../egg.js'; + +export function createLoggers(app: EggApplicationCore) { + const loggerOptions = { + ...app.config.logger, + type: app.type, + localStorage: app.ctxStorage, + } as EggLoggersOptions; + + // 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({ + logger: loggerOptions, + customLogger: app.config.customLogger, + }); + + // won't print to console after started, except for local and unittest + app.ready(() => { + if (app.config.logger.disableConsoleAfterReady) { + loggers.disableConsole(); + loggers.coreLogger.info('[egg:lib:core:logger] disable console log after app ready'); + } + }); + + // set global logger + for (const loggerName of Object.keys(loggers)) { + setCustomLogger(loggerName, loggers[loggerName]); + } + // reset global logger on beforeClose hook + 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', loggerOptions); + return loggers; +} diff --git a/src/lib/core/messenger/IMessenger.ts b/src/lib/core/messenger/IMessenger.ts new file mode 100644 index 0000000000..9aa752e7f3 --- /dev/null +++ b/src/lib/core/messenger/IMessenger.ts @@ -0,0 +1,58 @@ +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} workerId - the workerId of the receiver + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendTo(workerId: 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; + + onMessage(message: any): void; +} diff --git a/src/lib/core/messenger/index.ts b/src/lib/core/messenger/index.ts new file mode 100644 index 0000000000..e2473a2b0b --- /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 { EggApplicationCore } from '../../egg.js'; + +export type { IMessenger } from './IMessenger.js'; + +/** + * @class Messenger + */ +export function create(egg: EggApplicationCore): IMessenger { + return egg.options.mode === 'single' + ? new LocalMessenger(egg) + : new IPCMessenger(egg); +} diff --git a/src/lib/core/messenger/ipc.ts b/src/lib/core/messenger/ipc.ts new file mode 100644 index 0000000000..2e92ab53c8 --- /dev/null +++ b/src/lib/core/messenger/ipc.ts @@ -0,0 +1,148 @@ +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'; +import type { EggApplicationCore } from '../../egg.js'; + +const debug = debuglog('egg/lib/core/messenger/ipc'); + +/** + * Communication between app worker and agent worker by IPC channel + */ +export class Messenger extends EventEmitter implements IMessenger { + readonly pid: string; + readonly egg: EggApplicationCore; + opids: string[] = []; + + constructor(egg: EggApplicationCore) { + super(); + this.pid = String(process.pid); + this.egg = egg; + // 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.on('egg-pids', workerIds => { + debug('[%s:%s] got egg-pids %j', this.egg.type, this.pid, workerIds); + this.opids = workerIds.map((workerId: number) => String(workerId)); + }); + this.onMessage = this.onMessage.bind(this); + process.on('message', this.onMessage); + if (!workerThreads.isMainThread) { + workerThreads.parentPort!.on('message', this.onMessage); + } + } + + /** + * 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): Messenger { + debug('[%s:%s] broadcast %s with %j', this.egg.type, this.pid, action, data); + this.send(action, data, 'app'); + this.send(action, data, 'agent'); + return this; + } + + /** + * send message to the specified process + * @param {String} workerId - the workerId of the receiver + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendTo(workerId: string, action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to workerId:%s', this.egg.type, this.pid, action, data, workerId); + sendmessage(process, { + action, + data, + /** + * @deprecated Keep compatible, please use receiverWorkerId instead + */ + receiverPid: String(workerId), + receiverWorkerId: String(workerId), + }); + return this; + } + + /** + * 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): Messenger { + if (this.opids.length === 0) { + debug('[%s:%s] no pids, ignore sendRandom %s with %j', this.egg.type, this.pid, action, data); + return this; + } + const index = Math.floor(Math.random() * this.opids.length); + const workerId = this.opids[index]; + this.sendTo(workerId, action, data); + return this; + } + + /** + * send message to app + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToApp(action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to all app', this.egg.type, this.pid, action, data); + this.send(action, data, 'app'); + return this; + } + + /** + * send message to agent + * @param {String} action - message key + * @param {Object} data - message value + * @return {Messenger} this + */ + sendToAgent(action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to all agent', this.egg.type, this.pid, action, data); + this.send(action, data, 'agent'); + return this; + } + + /** + * @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): Messenger { + sendmessage(process, { + action, + data, + to, + }); + return this; + } + + onMessage(message: any) { + if (typeof message?.action === 'string') { + debug('[%s:%s] got message %s with %j, receiverWorkerId: %s', + this.egg.type, this.pid, message.action, message.data, message.receiverWorkerId ?? message.receiverPid); + this.emit(message.action, message.data); + } else { + debug('[%s:%s] got an invalid message %j', this.egg.type, this.pid, message); + } + } + + close() { + process.removeListener('message', this.onMessage); + this.removeAllListeners(); + } + + /** + * @function Messenger#on + * @param {String} action - message key + * @param {Object} data - message value + */ +} diff --git a/lib/core/messenger/local.js b/src/lib/core/messenger/local.ts similarity index 58% rename from lib/core/messenger/local.js rename to src/lib/core/messenger/local.ts index 7be992e0ee..2f9a93a4d7 100644 --- a/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 { EggApplicationCore } 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: EggApplicationCore; - constructor(egg) { + constructor(egg: EggApplicationCore) { super(); this.egg = egg; + this.pid = String(process.pid); } /** @@ -20,8 +24,8 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - broadcast(action, data) { - debug('[%s] broadcast %s with %j', this.pid, action, data); + broadcast(action: string, data?: unknown): Messenger { + debug('[%s:%s] broadcast %s with %j', this.egg.type, this.pid, action, data); this.send(action, data, 'both'); return this; } @@ -30,14 +34,16 @@ class Messenger extends EventEmitter { * send message to the specified process * Notice: in single process mode, it only can send to self process, * and it will send to both agent and app's messengers. - * @param {String} pid - the process id of the receiver + * @param {String} workerId - the workerId of the receiver * @param {String} action - message key * @param {Object} data - message value * @return {Messenger} this */ - sendTo(pid, action, data) { - debug('[%s] send %s with %j to %s', this.pid, action, data, pid); - if (pid !== process.pid) return this; + sendTo(workerId: string, action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to %s', this.egg.type, this.pid, action, data, workerId); + if (String(workerId) !== this.pid) { + return this; + } this.send(action, data, 'both'); return this; } @@ -51,8 +57,8 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendRandom(action, data) { - debug('[%s] send %s with %j to opposite', this.pid, action, data); + sendRandom(action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to opposite', this.egg.type, this.pid, action, data); this.send(action, data, 'opposite'); return this; } @@ -63,8 +69,8 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToApp(action, data) { - debug('[%s] send %s with %j to all app', this.pid, action, data); + sendToApp(action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to all app', this.egg.type, this.pid, action, data); this.send(action, data, 'application'); return this; } @@ -75,8 +81,8 @@ class Messenger extends EventEmitter { * @param {Object} data - message value * @return {Messenger} this */ - sendToAgent(action, data) { - debug('[%s] send %s with %j to all agent', this.pid, action, data); + sendToAgent(action: string, data?: unknown): Messenger { + debug('[%s:%s] send %s with %j to all agent', this.egg.type, 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,26 +110,30 @@ 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 }); + 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) { - if (message && is.string(message.action)) { - debug('[%s] got message %s with %j', this.pid, message.action, message.data); + onMessage(message: any) { + if (typeof message?.action === 'string') { + debug('[%s:%s] got message %s with %j', this.egg.type, this.pid, message.action, message.data); this.emit(message.action, message.data); + } else { + debug('[%s:%s] got an invalid message %j', this.egg.type, this.pid, message); } } @@ -137,5 +147,3 @@ class Messenger extends EventEmitter { * @param {Object} data - message value */ } - -module.exports = Messenger; diff --git a/lib/core/singleton.js b/src/lib/core/singleton.ts similarity index 54% rename from lib/core/singleton.js rename to src/lib/core/singleton.ts index d0b9c29e27..6d71611185 100644 --- a/lib/core/singleton.js +++ b/src/lib/core/singleton.ts @@ -1,36 +1,48 @@ -'use strict'; +import assert from 'node:assert'; +import { isAsyncFunction } from 'is-type-of'; +import type { EggApplicationCore } from '../egg.js'; -const assert = require('assert'); -const is = require('is-type-of'); +export type SingletonCreateMethod = + (config: Record, app: EggApplicationCore, clientName: string) => unknown | Promise; -class Singleton { - constructor(options = {}) { +export interface SingletonOptions { + name: string; + app: EggApplicationCore; + create: SingletonCreateMethod; +} + +export class Singleton { + readonly clients = new Map(); + readonly app: EggApplicationCore; + 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(); + 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; - /* 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() { 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._extendDynamicMethods(client); + this.#setClientToApp(client); + this.#extendDynamicMethods(client); return; } @@ -40,66 +52,76 @@ 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._extendDynamicMethods(client); + this.#setClientToApp(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)); })); - this.app[this.name] = this; + this.#setClientToApp(this); return; } // no config.clients and config.client - this.app[this.name] = this; + this.#setClientToApp(this); } - get(id) { + #setClientToApp(client: unknown) { + Reflect.set(this.app, this.name, client); + } + + 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 +135,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.coreLogger.warn( + '[egg:singleton] %s dynamic create is disabled because of client is un-extendable', + this.name); + this.app.coreLogger.warn(err); } } } - -module.exports = Singleton; 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/lib/egg.js b/src/lib/egg.ts similarity index 51% rename from lib/egg.js rename to src/lib/egg.ts index aabba225b4..03e552f994 100644 --- a/lib/egg.js +++ b/src/lib/egg.ts @@ -1,37 +1,150 @@ -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 http, { type IncomingMessage, type ServerResponse } from 'node:http'; +import inspector from 'node:inspector'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { + EggCore, + Request as EggCoreRequest, + Response as EggCoreResponse, + Router, +} from '@eggjs/core'; +import type { + EggCoreOptions, + Next, MiddlewareFunc as EggCoreMiddlewareFunc, + ILifecycleBoot, +} from '@eggjs/core'; +// 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'; +import Context, { type ContextDelegation } from '../app/extend/context.js'; +import type { EggAppConfig } from './type.js'; +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 } from './core/logger.js'; +import { + Singleton, type SingletonCreateMethod, type SingletonOptions, +} from './core/singleton.js'; +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'; +import { getSourceDirname } from './utils.js'; + +import './egg.types.js'; + const EGG_PATH = Symbol.for('egg#eggPath'); -const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); + +export interface EggApplicationCoreOptions extends Omit { + mode?: 'cluster' | 'single'; + clusterPort?: number; + baseDir?: string; +} + +export class Request extends EggCoreRequest { + declare app: EggCore; + declare response: Response; + declare ctx: ContextDelegation; +} + +export class Response extends EggCoreResponse { + declare app: EggCore; + declare request: Request; + declare ctx: ContextDelegation; +} + +// export egg types +export type { + ContextDelegation, + ILifecycleBoot, + Next, +}; +// keep compatible with egg version 3.x +export type EggContext = ContextDelegation; +export type MiddlewareFunc = EggCoreMiddlewareFunc; + +// export egg classes +export { + Context, + Router, + EggLogger, +}; /** * 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 EggApplicationCore extends EggCore { + declare ctxStorage: AsyncLocalStorage; + // 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; + + declare options: Required; + + #httpClient?: HttpClient; + #loggers?: EggLoggers; + #clusterClients: any[] = []; + + readonly messenger: IMessenger; + agent?: Agent; + application?: Application; + declare loader: EggApplicationLoader; /** * @class @@ -41,91 +154,73 @@ 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?: EggApplicationCoreOptions) { + 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(); - /** * messenger instance * @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 this.messenger.once('egg-ready', () => { this.lifecycle.triggerServerDidReady(); }); + this.lifecycle.registerBeforeStart(async () => { + await this.load(); + }, 'load files'); + } + /** + * @deprecated please use `options` property instead + */ + get _options() { + return this.options; + } + + 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:core] dump config after ready, %s', ms(Date.now() - dumpStartTime)); + this.coreLogger.info('[egg] dump config after ready, %sms', 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); - 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); 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 () => { + 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(); + await this.agent!.close(); } for (const logger of this.loggers.values()) { @@ -135,45 +230,43 @@ class EggApplication extends EggCore { 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; + await this.loader.load(); + } - /** - * 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: unknown, options?: object) { + const clientClassOptions = { + ...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 = createClusterClient(clientClass, clientClassOptions); + this.#patchClusterClient(client); + return client; } /** @@ -185,7 +278,7 @@ class EggApplication extends EggCore { * console.log(app); * => * { - * name: 'mockapp', + * name: 'mock-app', * env: 'test', * subdomainOffset: 2, * config: '', @@ -197,23 +290,21 @@ class EggApplication extends EggCore { * } * ``` */ - inspect() { + inspect(): any { const res = { env: this.config.env, }; - function delegate(res, app, keys) { + function delegate(res: any, app: any, keys: string[]) { for (const key of keys) { - /* istanbul ignore else */ if (app[key]) { res[key] = app[key]; } } } - function abbr(res, app, keys) { + function abbr(res: any, app: any, keys: string[]) { for (const key of keys) { - /* istanbul ignore else */ if (app[key]) { res[key] = ``; } @@ -250,13 +341,13 @@ 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`. - * 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 +357,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 +373,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 +382,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 +416,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,11 +438,10 @@ 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 - /* istanbul ignore else */ if (err) { if (err.name) newError.name = err.name; if (err.message) newError.message = err.message; @@ -364,7 +449,6 @@ class EggApplication extends EggCore { } err = newError; } - /* istanbul ignore else */ if (err.name === 'Error') { err.name = 'unhandledRejectionError'; } @@ -374,21 +458,23 @@ class EggApplication extends EggCore { /** * dump out the config and meta object * @private - * @return {Object} the result */ dumpConfigToObject() { - let ignoreList; + let ignoreList: (string | RegExp)[]; 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, + }); + convertObject(config, ignoreList); return { - config: json, + config, meta: this.loader.configMeta, }; } @@ -400,10 +486,11 @@ 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 dumpped object + // get dumped object const { config, meta } = this.dumpConfigToObject(); // dump config @@ -413,8 +500,8 @@ 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) { - this.coreLogger.warn(`dumpConfig error: ${err.message}`); + } catch (err: any) { + this.coreLogger.warn(`[egg] dumpConfig error: ${err.message}`); } } @@ -427,24 +514,24 @@ 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) { - this.coreLogger.warn('[egg:core][slow-boot-action] #%d %dms, name: %s', + if (item.index > 0 && item.duration && item.duration >= this.config.dump.timing.slowBootActionMinDuration) { + 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}`); + } catch (err: any) { + this.coreLogger.warn(`[egg] dumpTiming error: ${err.message}`); } } get [EGG_PATH]() { - return path.join(__dirname, '..'); + return getSourceDirname(); } - _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.`); @@ -454,7 +541,8 @@ 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(); @@ -462,6 +550,10 @@ class EggApplication extends EggCore { this.ready(() => clearTimeout(startTimeoutTimer)); } + get config() { + return super.config as EggAppConfig; + } + /** * app.env delegate app.config.env * @deprecated @@ -478,7 +570,7 @@ class EggApplication extends EggCore { * @deprecated */ get proxy() { - this.deprecate('please use app.config.proxy instead'); + // this.deprecate('please use app.config.proxy instead'); return this.config.proxy; } /* eslint no-empty-function: off */ @@ -489,11 +581,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) { @@ -503,12 +596,11 @@ 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; }; } @@ -520,8 +612,8 @@ 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) { - const request = { + createAnonymousContext(req?: any): EggContext { + const request: any = { headers: { host: '127.0.0.1', 'x-forwarded-for': '127.0.0.1', @@ -560,36 +652,20 @@ 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): 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 as any; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.onerror = context.onerror.bind(context); - context.originalUrl = request.originalUrl = req.url; - - /** - * Request start time - * @member {Number} Context#starttime - */ + context.originalUrl = request.originalUrl = req.url as string; context.starttime = Date.now(); - - if (this.config.logger.enablePerformanceTimer) { - /** - * Request start timer using `performance.now()` - * @member {Number} Context#performanceStarttime - */ - context.performanceStarttime = performance.now(); - } + context.performanceStarttime = performance.now(); return context; } - } - -module.exports = EggApplication; diff --git a/src/lib/egg.types.ts b/src/lib/egg.types.ts new file mode 100644 index 0000000000..acff8f030c --- /dev/null +++ b/src/lib/egg.types.ts @@ -0,0 +1,11 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { ContextDelegation } from '../app/extend/context.js'; + +declare module '@eggjs/core' { + // add EggApplicationCore overrides types + interface EggCore { + inspect(): any; + get currentContext(): ContextDelegation | undefined; + ctxStorage: AsyncLocalStorage; + } +} diff --git a/src/lib/loader/AgentWorkerLoader.ts b/src/lib/loader/AgentWorkerLoader.ts new file mode 100644 index 0000000000..3f6a0eae5a --- /dev/null +++ b/src/lib/loader/AgentWorkerLoader.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/AppWorkerLoader.ts b/src/lib/loader/AppWorkerLoader.ts new file mode 100644 index 0000000000..2328c839ee --- /dev/null +++ b/src/lib/loader/AppWorkerLoader.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/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/index.ts b/src/lib/loader/index.ts new file mode 100644 index 0000000000..0c4b332a11 --- /dev/null +++ b/src/lib/loader/index.ts @@ -0,0 +1,3 @@ +export { EggApplicationLoader } from './EggApplicationLoader.js'; +export * from './AppWorkerLoader.js'; +export * from './AgentWorkerLoader.js'; diff --git a/src/lib/start.ts b/src/lib/start.ts new file mode 100644 index 0000000000..9a94a10573 --- /dev/null +++ b/src/lib/start.ts @@ -0,0 +1,56 @@ +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 StartEggOptions { + /** 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'; + env?: string; +} + +/** + * Start egg with single process + */ +export async function startEgg(options: StartEggOptions = {}) { + 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({ + ...options, + }); + await agent.ready(); + const application = new ApplicationClass({ + ...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/type.ts b/src/lib/type.ts new file mode 100644 index 0000000000..4d815f8ffb --- /dev/null +++ b/src/lib/type.ts @@ -0,0 +1,329 @@ +import type { Socket } from 'node:net'; +import type { + RequestOptions as HttpClientRequestOptions, +} from 'urllib'; +import type { + EggLoggerOptions, EggLoggersOptions, +} from 'egg-logger'; +import type { + FileLoaderOptions, +} from '@eggjs/core'; +import type { + EggApplicationCore, ContextDelegation, +} 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'; + +// import @eggjs/watcher types +// import '@eggjs/watcher'; + +type IgnoreItem = string | RegExp | ((ctx: ContextDelegation) => boolean); +type IgnoreOrMatch = IgnoreItem | IgnoreItem[]; + +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 */ +export 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; + /** [deprecated] Defaults to `true`. */ + 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: ContextDelegation): 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} 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: EggApplicationCore): 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; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000000..597863c8d4 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,16 @@ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export function getSourceDirname() { + if (typeof __dirname !== 'undefined') { + return path.dirname(__dirname); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return path.dirname(path.dirname(fileURLToPath(import.meta.url))); +} + +export function getSourceFile(filename: string) { + return path.join(getSourceDirname(), filename); +} diff --git a/src/urllib.ts b/src/urllib.ts new file mode 100644 index 0000000000..38a7f532e3 --- /dev/null +++ b/src/urllib.ts @@ -0,0 +1 @@ +export * from 'urllib'; diff --git a/test/agent.test.js b/test/agent.test.js deleted file mode 100644 index 5cd9a2ae01..0000000000 --- a/test/agent.test.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const mock = require('egg-mock'); -const path = require('path'); -const utils = require('./utils'); - -describe('test/agent.test.js', () => { - afterEach(mock.restore); - let app; - - before(() => { - app = utils.app('apps/agent-logger-config'); - return app.ready(); - }); - after(() => app.close()); - - it('agent logger config should work', () => { - const fileTransport = app._agent.logger.get('file'); - assert(fileTransport.options.file === path.join('/tmp/foo', 'egg-agent.log')); - }); -}); diff --git a/test/agent.test.ts b/test/agent.test.ts new file mode 100644 index 0000000000..8eb8225a0b --- /dev/null +++ b/test/agent.test.ts @@ -0,0 +1,19 @@ +import { strict as assert } from 'node:assert'; +import path from 'node:path'; +import { createApp, MockApplication, restore } from './utils.js'; + +describe('test/agent.test.ts', () => { + afterEach(restore); + let app: MockApplication; + + before(() => { + app = createApp('apps/agent-logger-config'); + return app.ready(); + }); + after(() => app.close()); + + it('agent logger config should work', () => { + const fileTransport = app._agent.logger.get('file'); + assert.equal(fileTransport.options.file, path.join('/tmp/foo', 'egg-agent.log')); + }); +}); diff --git a/test/app/extend/agent.test.js b/test/app/extend/agent.test.ts similarity index 65% rename from test/app/extend/agent.test.js rename to test/app/extend/agent.test.ts index e15682f8a7..e8868bc337 100644 --- a/test/app/extend/agent.test.js +++ b/test/app/extend/agent.test.ts @@ -1,17 +1,13 @@ -'use strict'; +import { strict as assert } from 'node:assert'; +import { restore, createApp, MockApplication } from '../../utils.js'; -const assert = require('assert'); - -const mm = require('egg-mock'); -const utils = require('../../utils'); - -describe('test/app/extend/agent.test.js', () => { - afterEach(mm.restore); +describe('test/app/extend/agent.test.ts', () => { + afterEach(restore); describe('agent.addSingleton()', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/singleton-demo'); + app = createApp('apps/singleton-demo'); return app.ready(); }); after(() => app.close()); @@ -21,28 +17,28 @@ describe('test/app/extend/agent.test.js', () => { assert(config.foo === 'bar'); assert(config.foo2 === 'bar2'); - const ds = app.agent.dataService.createInstance({ foo: 'barrr' }); + const ds = app.agent.dataService.createInstance({ foo: 'bar2' }); config = await ds.getConfig(); - assert(config.foo === 'barrr'); + assert(config.foo === 'bar2'); - const ds2 = await app.agent.dataService.createInstanceAsync({ foo: 'barrr' }); + const ds2 = await app.agent.dataService.createInstanceAsync({ foo: 'bar2' }); config = await ds2.getConfig(); - assert(config.foo === 'barrr'); + assert(config.foo === 'bar2'); config = await app.agent.dataServiceAsync.get('second').getConfig(); assert(config.foo === 'bar'); assert(config.foo2 === 'bar2'); try { - app.agent.dataServiceAsync.createInstance({ foo: 'barrr' }); + app.agent.dataServiceAsync.createInstance({ foo: 'bar2' }); throw new Error('should not execute'); - } catch (err) { + } catch (err: any) { assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); } - const ds4 = await app.agent.dataServiceAsync.createInstanceAsync({ foo: 'barrr' }); + const ds4 = await app.agent.dataServiceAsync.createInstanceAsync({ foo: 'bar2' }); config = await ds4.getConfig(); - assert(config.foo === 'barrr'); + assert(config.foo === 'bar2'); }); }); }); diff --git a/test/app/extend/application.test.js b/test/app/extend/application.test.ts similarity index 73% rename from test/app/extend/application.test.js rename to test/app/extend/application.test.ts index 4b93626fa7..4bdae4a6be 100644 --- a/test/app/extend/application.test.js +++ b/test/app/extend/application.test.ts @@ -1,14 +1,15 @@ -const assert = require('assert'); -const fs = require('fs'); -const path = require('path'); -const utils = require('../../utils'); +import { strict as assert } from 'node:assert'; +import fs from 'node:fs'; +import path from 'node:path'; +import { scheduler } from 'node:timers/promises'; +import { createApp, MockApplication, cluster } from '../../utils.js'; -describe('test/app/extend/application.test.js', () => { +describe('test/app/extend/application.test.ts', () => { describe('app.logger', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); @@ -17,23 +18,23 @@ describe('test/app/extend/application.test.js', () => { assert(app.logger === app.loggers.logger); }); - it('should alias app.coreLooger => app.loggers.coreLooger', () => { + it('should alias app.coreLogger => app.loggers.coreLogger', () => { assert(app.coreLogger === app.loggers.coreLogger); }); - it('should alias app.getLogger(\'coreLogger\') => app.loggers.coreLooger', () => { + it('should alias app.getLogger(\'coreLogger\') => app.loggers.coreLogger', () => { assert(app.getLogger('coreLogger') === app.loggers.coreLogger); }); - it('should alias app.getLogger(\'noexist\') => null', () => { - assert(app.getLogger('noexist') === null); + it('should alias app.getLogger(\'noExist\') => null', () => { + assert(app.getLogger('noExist') === null); }); }); describe('app.inspect()', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); @@ -50,22 +51,22 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.readyCallback()', () => { - let app; + let app: MockApplication; after(() => app.close()); it('should log info when plugin is not ready', async () => { - app = utils.cluster('apps/notready'); + app = cluster('apps/notready'); // it won't be ready, so wait for the timeout - await utils.sleep(11000); + await scheduler.wait(11000); app.expect('stderr', /\[egg:core:ready_timeout] 10 seconds later a was still unable to finish./); }); }); describe('app.locals', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/locals'); + app = createApp('apps/locals'); return app.ready(); }); after(() => app.close()); @@ -84,15 +85,15 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.locals.foo = bar', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/app-locals-getter'); + app = createApp('apps/app-locals-getter'); return app.ready(); }); after(() => app.close()); - it('should work', () => { - return app.httpRequest() + it('should work', async () => { + return await app.httpRequest() .get('/test') .expect({ locals: { @@ -104,9 +105,9 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.createAnonymousContext()', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); @@ -120,7 +121,7 @@ describe('test/app/extend/application.test.js', () => { 'x-forwarded-for': '10.0.0.1', }, url: '/foobar?ok=1', - }); + } as any); assert(ctx.ip === '10.0.0.1'); assert(ctx.url === '/foobar?ok=1'); assert(ctx.socket.remoteAddress === '10.0.0.1'); @@ -129,9 +130,9 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.addSingleton()', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/singleton-demo'); + app = createApp('apps/singleton-demo'); return app.ready(); }); after(() => app.close()); @@ -156,7 +157,7 @@ describe('test/app/extend/application.test.js', () => { try { app.dataServiceAsync.createInstance({ foo: 'barrr' }); throw new Error('should not execute'); - } catch (err) { + } catch (err: any) { assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); } @@ -167,9 +168,9 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.runInBackground(scope)', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/ctx-background'); + app = createApp('apps/ctx-background'); return app.ready(); }); after(() => app.close()); @@ -179,62 +180,62 @@ describe('test/app/extend/application.test.js', () => { .get('/app_background') .expect(200) .expect('hello app'); - await utils.sleep(2100); + await scheduler.wait(2100); const logdir = app.config.logger.dir; const log = fs.readFileSync(path.join(logdir, 'ctx-background-web.log'), 'utf8'); assert(/mock background run at app result file size: \d+/.test(log)); assert(/mock background run at app anonymous result file size: \d+/.test(log)); assert( - /\[egg:background] task:.*?app[\/\\]controller[\/\\]app\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:.*?app[\/\\]controller[\/\\]app\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')), ); }); }); describe('app.runInAnonymousContextScope(scope)', () => { it('should run task in anonymous context scope success', async () => { - const app = utils.app('apps/app-runInAnonymousContextScope'); + const app = createApp('apps/app-runInAnonymousContextScope'); await app.ready(); await app.close(); - await utils.sleep(2100); + await scheduler.wait(2100); const logdir = app.config.logger.dir; const logs = fs.readFileSync(path.join(logdir, 'app-runInAnonymousContextScope-web.log'), 'utf8').split('\n'); // console.log(logs); // 2022-12-15 23:00:08,551 INFO 86728 [-/127.0.0.1/-/1ms GET /] before close on ctx logger // 2022-12-15 23:00:08,551 INFO 86728 [-/127.0.0.1/-/1ms GET /] before close on app logger // 2022-12-15 23:03:16,086 INFO 89216 outside before close on app logger - assert.match(logs[0], / INFO \d+ \[-\/127.0.0.1\/-\/\d+ms GET \/] inside before close on ctx logger/); - assert.match(logs[1], / INFO \d+ \[-\/127.0.0.1\/-\/\d+ms GET \/] inside before close on app logger/); + assert.match(logs[0], / INFO \d+ \[-\/127.0.0.1\/-\/[\d\.]+ms GET \/] inside before close on ctx logger/); + assert.match(logs[1], / INFO \d+ \[-\/127.0.0.1\/-\/[\d\.]+ms GET \/] inside before close on app logger/); assert.match(logs[2], / INFO \d+ outside before close on app logger/); }); }); describe('app.runInAnonymousContextScope(scope,request)', () => { it('should run task in anonymous context scope with req success', async () => { - const app = utils.app('apps/app-runInAnonymousContextScope-withRequest'); + const app = createApp('apps/app-runInAnonymousContextScope-withRequest'); await app.ready(); await app.close(); - await utils.sleep(2100); + await scheduler.wait(2100); const logdir = app.config.logger.dir; const logs = fs.readFileSync(path.join(logdir, 'app-runInAnonymousContextScope-withRequest-web.log'), { encoding: 'utf8' }).split('\n'); - assert.match(logs[0], / INFO \d+ \[-\/127.0.0.2\/-\/\d+ms GET \/] inside before close on ctx logger/); - assert.match(logs[1], / INFO \d+ \[-\/127.0.0.2\/-\/\d+ms GET \/] inside before close on app logger/); + assert.match(logs[0], / INFO \d+ \[-\/127.0.0.2\/-\/[\d\.]+ms GET \/] inside before close on ctx logger/); + assert.match(logs[1], / INFO \d+ \[-\/127.0.0.2\/-\/[\d\.]+ms GET \/] inside before close on app logger/); assert.match(logs[2], / INFO \d+ outside before close on app logger/); }); }); describe('app.handleRequest(ctx, fnMiddleware)', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); it('should wait for middleware resolution', async () => { const ctx = app.createAnonymousContext(); - await app.handleRequest(ctx, async ctx => { - await utils.sleep(100); + await (app as any).handleRequest(ctx, async (ctx: any) => { + await scheduler.wait(100); ctx.body = 'middleware resolution'; }); assert(ctx.body === 'middleware resolution'); @@ -242,9 +243,9 @@ describe('test/app/extend/application.test.js', () => { }); describe('app.keys', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); @@ -252,8 +253,9 @@ describe('test/app/extend/application.test.js', () => { it('should work for app.keys and app.keys=', async () => { assert.deepEqual(app.keys, [ 'foo' ]); // `app.keys=` will be ignored - app.keys = undefined; - assert.deepEqual(app.keys, [ 'foo' ]); + // TypeError: Cannot set property keys of # which has only a getter + // app.keys = undefined; + // assert.deepEqual(app.keys, [ 'foo' ]); }); }); }); diff --git a/test/app/extend/context.jsonp.test.js b/test/app/extend/context.jsonp.test.ts similarity index 84% rename from test/app/extend/context.jsonp.test.js rename to test/app/extend/context.jsonp.test.ts index 2b5319b2e2..e75edc2c9f 100644 --- a/test/app/extend/context.jsonp.test.js +++ b/test/app/extend/context.jsonp.test.ts @@ -1,16 +1,12 @@ -'use strict'; +import { createApp, MockApplication } from '../../utils.js'; -const mm = require('egg-mock'); -const utils = require('../../utils'); - -describe('test/app/extend/context.jsonp.test.js', () => { - let app; +describe('test/app/extend/context.jsonp.test.ts', () => { + let app: MockApplication; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); - afterEach(mm.restore); it('should response jsonp', () => { return app.httpRequest() diff --git a/test/app/extend/context.test.js b/test/app/extend/context.test.ts similarity index 75% rename from test/app/extend/context.test.js rename to test/app/extend/context.test.ts index 57aa32d4a5..269447ce6e 100644 --- a/test/app/extend/context.test.js +++ b/test/app/extend/context.test.ts @@ -1,39 +1,42 @@ -const fs = require('fs'); -const path = require('path'); -const mm = require('egg-mock'); -const assert = require('assert'); -const utils = require('../../utils'); - -describe('test/app/extend/context.test.js', () => { - afterEach(mm.restore); +import fs from 'node:fs'; +import path from 'node:path'; +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { + createApp, restore, MockApplication, mm, getFilepath, singleProcessApp, + startLocalServer, +} from '../../utils.js'; + +describe('test/app/extend/context.test.ts', () => { + afterEach(restore); + let app: MockApplication; describe('ctx.logger', () => { - let app; afterEach(() => app.close()); it('env=local: level => info', async () => { mm.env('local'); mm.consoleLevel('NONE'); - app = utils.app('apps/demo', { cache: false }); + app = createApp('apps/demo', { cache: false }); await app.ready(); - const logdir = app.config.logger.dir; + const logDir = app.config.logger.dir; await app.httpRequest() .get('/logger?message=foo') .expect('logger'); - await utils.sleep(5000); + await scheduler.wait(1200); - const errorContent = fs.readFileSync(path.join(logdir, 'common-error.log'), 'utf8'); + const errorContent = fs.readFileSync(path.join(logDir, 'common-error.log'), 'utf8'); assert(errorContent.includes('nodejs.Error: error foo')); assert(errorContent.includes('nodejs.Error: core error foo')); - const loggerContent = fs.readFileSync(path.join(logdir, 'demo-web.log'), 'utf8'); + const loggerContent = fs.readFileSync(path.join(logDir, 'demo-web.log'), 'utf8'); // loggerContent.should.containEql('debug foo'); assert(loggerContent.includes('info foo')); assert(loggerContent.includes('warn foo')); - const coreLoggerContent = fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8'); + const coreLoggerContent = fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8'); // coreLoggerContent.should.containEql('core debug foo'); assert(coreLoggerContent.includes('core info foo')); assert(coreLoggerContent.includes('core warn foo')); @@ -42,9 +45,9 @@ describe('test/app/extend/context.test.js', () => { it('env=unittest: level => info', async () => { mm.env('unittest'); mm.consoleLevel('NONE'); - app = utils.app('apps/demo', { cache: false }); + app = createApp('apps/demo', { cache: false }); await app.ready(); - const logdir = app.config.logger.dir; + const logDir = app.config.logger.dir; app.mockContext({ userId: '123123', @@ -54,19 +57,19 @@ describe('test/app/extend/context.test.js', () => { .get('/logger?message=foo') .expect('logger'); - await utils.sleep(5000); + await scheduler.wait(1200); - const errorContent = fs.readFileSync(path.join(logdir, 'common-error.log'), 'utf8'); + const errorContent = fs.readFileSync(path.join(logDir, 'common-error.log'), 'utf8'); assert(errorContent.includes('nodejs.Error: error foo')); assert(errorContent.includes('nodejs.Error: core error foo')); - assert(/\[123123\/[\d.]+\/-\/\d+ms GET \/logger\?message=foo]/.test(errorContent)); + assert.match(errorContent, /\[123123\/[\d\.]+\/-\/[\d\.]+ms GET \/logger\?message=foo]/); - const loggerContent = fs.readFileSync(path.join(logdir, 'demo-web.log'), 'utf8'); + const loggerContent = fs.readFileSync(path.join(logDir, 'demo-web.log'), 'utf8'); assert(!loggerContent.includes('debug foo')); assert(loggerContent.includes('info foo')); assert(loggerContent.includes('warn foo')); - const coreLoggerContent = fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8'); + const coreLoggerContent = fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8'); assert(!coreLoggerContent.includes('core debug foo')); assert(coreLoggerContent.includes('core info foo')); assert(coreLoggerContent.includes('core warn foo')); @@ -75,26 +78,26 @@ describe('test/app/extend/context.test.js', () => { it('env=prod: level => info', async () => { mm.env('unittest'); mm.consoleLevel('NONE'); - app = utils.app('apps/demo', { cache: false }); + app = createApp('apps/demo', { cache: false }); await app.ready(); - const logdir = app.config.logger.dir; + const logDir = app.config.logger.dir; await app.httpRequest() .get('/logger?message=foo') .expect('logger'); - await utils.sleep(5000); + await scheduler.wait(2000); - const errorContent = fs.readFileSync(path.join(logdir, 'common-error.log'), 'utf8'); + const errorContent = fs.readFileSync(path.join(logDir, 'common-error.log'), 'utf8'); assert(errorContent.includes('nodejs.Error: error foo')); assert(errorContent.includes('nodejs.Error: core error foo')); - const loggerContent = fs.readFileSync(path.join(logdir, 'demo-web.log'), 'utf8'); + const loggerContent = fs.readFileSync(path.join(logDir, 'demo-web.log'), 'utf8'); assert(!loggerContent.includes('debug foo')); assert(loggerContent.includes('info foo')); assert(loggerContent.includes('warn foo')); - const coreLoggerContent = fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8'); + const coreLoggerContent = fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8'); assert(!coreLoggerContent.includes('core debug foo')); assert(coreLoggerContent.includes('core info foo')); assert(coreLoggerContent.includes('core warn foo')); @@ -102,9 +105,8 @@ describe('test/app/extend/context.test.js', () => { }); describe('ctx.getLogger', () => { - let app; before(() => { - app = utils.app('apps/get-logger'); + app = createApp('apps/get-logger'); return app.ready(); }); after(() => app.close()); @@ -120,18 +122,18 @@ describe('test/app/extend/context.test.js', () => { .get('/logger') .expect(200); - await utils.sleep(100); - const logPath = utils.getFilepath('apps/get-logger/logs/get-logger/a.log'); - assert( - /\[-\/127.0.0.1\/-\/\d+ms GET \/logger] aaa/.test(fs.readFileSync(logPath, 'utf8')) + await scheduler.wait(100); + const logPath = getFilepath('apps/get-logger/logs/get-logger/a.log'); + assert.match( + fs.readFileSync(logPath, 'utf8'), + /\[-\/127.0.0.1\/-\/[\d\.]+ms GET \/logger] aaa/, ); }); }); describe('app or framework can override ctx.getLogger', () => { - let app; before(() => { - app = utils.app('apps/custom-context-getlogger'); + app = createApp('apps/custom-context-getlogger'); return app.ready(); }); after(() => app.close()); @@ -144,9 +146,8 @@ describe('test/app/extend/context.test.js', () => { }); describe('agent anonymous context can be extended', () => { - let app; before(() => { - app = utils.app('apps/custom-context-getlogger'); + app = createApp('apps/custom-context-getlogger'); return app.ready(); }); after(() => app.close()); @@ -159,14 +160,13 @@ describe('test/app/extend/context.test.js', () => { }); describe('properties', () => { - let app; before(() => { - app = utils.app('apps/context-config-app'); + app = createApp('apps/context-config-app'); return app.ready(); }); after(() => app.close()); - describe('ctx.router getter and settter', () => { + describe('ctx.router getter and setter', () => { it('should work', () => { return app.httpRequest() .get('/') @@ -177,9 +177,8 @@ describe('test/app/extend/context.test.js', () => { }); describe('ctx.locals', () => { - let app; before(() => { - app = utils.app('apps/locals'); + app = createApp('apps/locals'); return app.ready(); }); after(() => app.close()); @@ -237,9 +236,8 @@ describe('test/app/extend/context.test.js', () => { }); describe('ctx.runInBackground(scope)', () => { - let app; before(() => { - app = utils.app('apps/ctx-background'); + app = createApp('apps/ctx-background'); return app.ready(); }); after(() => app.close()); @@ -250,16 +248,16 @@ describe('test/app/extend/context.test.js', () => { .expect(200) .expect('hello'); await app.backgroundTasksFinished(); - await utils.sleep(100); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'ctx-background-web.log'), 'utf8'); + await scheduler.wait(100); + const logDir = app.config.logger.dir; + const log = fs.readFileSync(path.join(logDir, 'ctx-background-web.log'), 'utf8'); assert(/background run result file size: \d+/.test(log)); assert(/background run anonymous result file size: \d+/.test(log)); assert( - /\[egg:background] task:saveUserInfo success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:saveUserInfo success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); assert( - /\[egg:background] task:.*?app[\/\\]controller[\/\\]home\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:.*?app[\/\\]controller[\/\\]home\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); }); @@ -269,12 +267,12 @@ describe('test/app/extend/context.test.js', () => { .expect(200) .expect('hello'); await app.backgroundTasksFinished(); - await utils.sleep(100); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'ctx-background-web.log'), 'utf8'); + await scheduler.wait(100); + const logDir = app.config.logger.dir; + const log = fs.readFileSync(path.join(logDir, 'ctx-background-web.log'), 'utf8'); assert(/background run result file size: \d+/.test(log)); assert( - /\[egg:background] task:customTaskName success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:customTaskName success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); }); @@ -282,7 +280,7 @@ describe('test/app/extend/context.test.js', () => { mm.consoleLevel('NONE'); let errorHadEmit = false; - app.on('error', (err, ctx) => { + app.on('error', (err: any, ctx: any) => { assert(err.runInBackground); assert(/ENOENT: no such file or directory/.test(err.message)); assert(ctx); @@ -293,13 +291,13 @@ describe('test/app/extend/context.test.js', () => { .expect(200) .expect('hello error'); await app.backgroundTasksFinished(); - await utils.sleep(100); + await scheduler.wait(100); assert(errorHadEmit); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'common-error.log'), 'utf8'); + const lgoDir = app.config.logger.dir; + const log = fs.readFileSync(path.join(lgoDir, 'common-error.log'), 'utf8'); assert(/ENOENT: no such file or directory/.test(log)); assert( - /\[egg:background] task:mockError fail \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:mockError fail \([\d\.]+ms\)/.test(fs.readFileSync(path.join(lgoDir, 'egg-web.log'), 'utf8')), ); }); @@ -313,11 +311,11 @@ describe('test/app/extend/context.test.js', () => { }); describe('ctx.runInBackground(scope) with single process mode', () => { - // ctx.runInBackground with egg-mock are overrided + // ctx.runInBackground with @eggjs/mock are override // single process mode will use the original ctx.runInBackground - let app; + let app: MockApplication; before(async () => { - app = await utils.singleProcessApp('apps/ctx-background'); + app = await singleProcessApp('apps/ctx-background'); }); after(() => app.close()); @@ -326,16 +324,16 @@ describe('test/app/extend/context.test.js', () => { .get('/') .expect(200) .expect('hello'); - await utils.sleep(5000); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'ctx-background-web.log'), 'utf8'); + await scheduler.wait(1200); + const logDir = app.config.logger.dir!; + const log = fs.readFileSync(path.join(logDir, 'ctx-background-web.log'), 'utf8'); assert(/background run result file size: \d+/.test(log)); assert(/background run anonymous result file size: \d+/.test(log)); assert( - /\[egg:background] task:saveUserInfo success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:saveUserInfo success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); assert( - /\[egg:background] task:.*?app[\/\\]controller[\/\\]home\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:.*?app[\/\\]controller[\/\\]home\.js:\d+:\d+ success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); }); @@ -344,12 +342,12 @@ describe('test/app/extend/context.test.js', () => { .get('/custom') .expect(200) .expect('hello'); - await utils.sleep(5000); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'ctx-background-web.log'), 'utf8'); + await scheduler.wait(1200); + const logDir = app.config.logger.dir!; + const log = fs.readFileSync(path.join(logDir, 'ctx-background-web.log'), 'utf8'); assert(/background run result file size: \d+/.test(log)); assert( - /\[egg:background] task:customTaskName success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:customTaskName success \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); }); @@ -367,35 +365,34 @@ describe('test/app/extend/context.test.js', () => { .get('/error') .expect(200) .expect('hello error'); - await utils.sleep(5000); + await scheduler.wait(1200); assert(errorHadEmit); - const logdir = app.config.logger.dir; - const log = fs.readFileSync(path.join(logdir, 'common-error.log'), 'utf8'); + const logDir = app.config.logger.dir!; + const log = fs.readFileSync(path.join(logDir, 'common-error.log'), 'utf8'); assert(/ENOENT: no such file or directory/.test(log)); assert( - /\[egg:background] task:mockError fail \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logdir, 'egg-web.log'), 'utf8')) + /\[egg:background] task:mockError fail \([\d\.]+ms\)/.test(fs.readFileSync(path.join(logDir, 'egg-web.log'), 'utf8')), ); }); }); describe('tests on apps/demo', () => { - let app; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); describe('ctx.curl()', () => { it('should curl ok', async () => { - const localServer = await utils.startLocalServer(); + const localServer = await startLocalServer(); const context = app.mockContext(); const res = await context.curl(`${localServer}/foo/bar`); assert(res.status === 200); }); it('should curl as promise ok', () => { - return utils.startLocalServer() + return startLocalServer() .then(localServer => app.mockContext().curl(`${localServer}/foo/bar`)) .then(res => assert(res.status === 200)); }); @@ -409,6 +406,11 @@ describe('test/app/extend/context.test.js', () => { assert(typeof ctx.httpclient.request === 'function'); assert(typeof ctx.httpclient.curl === 'function'); }); + + it('should httpclient alias to httpClient', async () => { + const ctx = app.mockContext(); + assert.equal(ctx.httpclient, ctx.httpClient); + }); }); describe('ctx.realStatus', () => { @@ -428,7 +430,7 @@ describe('test/app/extend/context.test.js', () => { describe('ctx.state', () => { it('should delegate ctx.locals', () => { - const context = app.mockContext(); + const context = app.mockContext() as any; context.locals = { a: 'a', b: 'b' }; context.state = { a: 'aa', c: 'cc' }; assert.deepEqual(context.state, { a: 'aa', b: 'b', c: 'cc' }); @@ -480,12 +482,13 @@ describe('test/app/extend/context.test.js', () => { describe('get router()', () => { it('should alias to app.router', () => { const ctx = app.mockContext(); - assert(ctx.router === app.router); + assert.equal(ctx.router, app.router); }); + it('should work with setter app.router', () => { const ctx = app.mockContext(); - ctx.router = 'router'; - assert(ctx.router === 'router'); + (ctx as any).router = 'router'; + assert.equal(ctx.router, 'router'); }); }); }); diff --git a/test/app/extend/helper.test.js b/test/app/extend/helper.test.ts similarity index 88% rename from test/app/extend/helper.test.js rename to test/app/extend/helper.test.ts index e707c4a4c4..56dfc8b159 100644 --- a/test/app/extend/helper.test.js +++ b/test/app/extend/helper.test.ts @@ -1,11 +1,9 @@ -'use strict'; +import { createApp, MockApplication } from '../../utils.js'; -const utils = require('../../utils'); - -describe('test/app/extend/helper.test.js', () => { - let app; +describe('test/app/extend/helper.test.ts', () => { + let app: MockApplication; before(() => { - app = utils.app('apps/helper'); + app = createApp('apps/helper'); return app.ready(); }); after(() => app.close()); diff --git a/test/app/extend/request.test.js b/test/app/extend/request.test.ts similarity index 89% rename from test/app/extend/request.test.js rename to test/app/extend/request.test.ts index ca54d6fdbb..6be80126a1 100644 --- a/test/app/extend/request.test.js +++ b/test/app/extend/request.test.ts @@ -1,17 +1,16 @@ -'use strict'; +import { strict as assert } from 'node:assert'; +import { once } from 'node:events'; +import type { AddressInfo } from 'node:net'; +import urllib from 'urllib'; +import { createApp, MockApplication, restore, mm } from '../../utils.js'; -const assert = require('assert'); -const mm = require('egg-mock'); -const urllib = require('urllib'); -const utils = require('../../utils'); - -describe('test/app/extend/request.test.js', () => { +describe('test/app/extend/request.test.ts', () => { describe('normal', () => { - let app; + let app: MockApplication; let ctx; - let req; + let req: any; before(() => { - app = utils.app('apps/demo'); + app = createApp('apps/demo'); return app.ready(); }); after(() => app.close()); @@ -19,7 +18,7 @@ describe('test/app/extend/request.test.js', () => { ctx = app.mockContext(); req = ctx.request; }); - afterEach(mm.restore); + afterEach(restore); describe('req.host', () => { it('should return host with port', () => { @@ -180,8 +179,8 @@ describe('test/app/extend/request.test.js', () => { it('should return value from socket.encrypted', () => { const ctx = app.mockContext(); - ctx.request.socket.encrypted = true; - assert(ctx.request.protocol === 'https'); + (ctx.request as any).socket.encrypted = true; + assert.equal(ctx.request.protocol, 'https'); }); }); @@ -208,7 +207,7 @@ describe('test/app/extend/request.test.js', () => { }); describe('this.query[key] => String', () => { - function expectQuery(querystring, query) { + function expectQuery(querystring: any, query: any) { mm(req, 'querystring', querystring); assert.deepEqual(req.query, query); mm.restore(); @@ -240,7 +239,7 @@ describe('test/app/extend/request.test.js', () => { }); describe('this.queries[key] => Array', () => { - function expectQueries(querystring, query) { + function expectQueries(querystring: any, query: any) { mm(req, 'querystring', querystring); assert.deepEqual(req.queries, query); mm.restore(); @@ -370,41 +369,36 @@ describe('test/app/extend/request.test.js', () => { }); describe('work with egg app', () => { - let app; - let host; + let app: MockApplication; + let host: string; before(() => { - app = utils.app('apps/querystring-extended'); + app = createApp('apps/querystring-extended'); return app.ready(); }); - before(done => { - app.listen(0, function() { - host = `http://127.0.0.1:${this.address().port}`; - done(); - }); + before(async () => { + const server = app.listen(0); + await once(server, 'listening'); + host = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; }); after(() => app.close()); - it('should return query and queries', done => { - urllib.request(`${host}/?p=a,b&p=b,c&a[foo]=bar`, { + it('should return query and queries', async () => { + const res = await urllib.request(`${host}/?p=a,b&p=b,c&a[foo]=bar`, { dataType: 'json', - }, (err, body) => { - assert.deepEqual(body, { - query: { p: 'a,b', 'a[foo]': 'bar' }, - queries: { p: [ 'a,b', 'b,c' ], 'a[foo]': [ 'bar' ] }, - }); - done(err); + }); + assert.deepEqual(res.data, { + query: { p: 'a,b', 'a[foo]': 'bar' }, + queries: { p: [ 'a,b', 'b,c' ], 'a[foo]': [ 'bar' ] }, }); }); - it('should work with encodeURIComponent', done => { - urllib.request(`${host}/?p=a,b&p=b,c&${encodeURIComponent('a[foo]')}=bar`, { + it('should work with encodeURIComponent', async () => { + const res = await urllib.request(`${host}/?p=a,b&p=b,c&${encodeURIComponent('a[foo]')}=bar`, { dataType: 'json', - }, (err, body) => { - assert.deepEqual(body, { - query: { p: 'a,b', 'a[foo]': 'bar' }, - queries: { p: [ 'a,b', 'b,c' ], 'a[foo]': [ 'bar' ] }, - }); - done(err); + }); + assert.deepEqual(res.data, { + query: { p: 'a,b', 'a[foo]': 'bar' }, + queries: { p: [ 'a,b', 'b,c' ], 'a[foo]': [ 'bar' ] }, }); }); }); diff --git a/test/app/extend/response.test.js b/test/app/extend/response.test.js deleted file mode 100644 index 6c38dc21c2..0000000000 --- a/test/app/extend/response.test.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const mm = require('egg-mock'); -const utils = require('../../utils'); - -describe('test/app/extend/response.test.js', () => { - afterEach(mm.restore); - - describe('length and type', () => { - let app; - before(() => { - app = utils.app('apps/response'); - return app.ready(); - }); - after(() => app.close()); - - it('should get lower case header', () => { - return app.httpRequest() - .get('/') - .expect(200) - .expect(res => { - assert(res.res.rawHeaders.indexOf('content-type') >= 0); - assert(res.res.rawHeaders.indexOf('content-length') >= 0); - }); - }); - - it('should get body length', () => { - const ctx = app.mockContext(); - ctx.body = null; - ctx.response.remove('content-length'); - assert(ctx.response.length === undefined); - ctx.body = '1'; - ctx.response.remove('content-length'); - assert(ctx.response.length === 1); - ctx.body = Buffer.alloc(2); - ctx.response.remove('content-length'); - assert(ctx.response.length === 2); - ctx.body = {}; - ctx.response.remove('content-length'); - assert(ctx.response.length === 2); - // mock stream - ctx.body = { pipe() {} }; - ctx.response.remove('content-length'); - assert(ctx.response.length === undefined); - }); - }); - - describe('test on apps/demo', () => { - let app; - before(() => { - app = utils.app('apps/demo'); - return app.ready(); - }); - after(() => app.close()); - - describe('response.realStatus', () => { - it('should get from status ok', () => { - const ctx = app.mockContext(); - ctx.response.status = 200; - assert(ctx.response.realStatus === 200); - assert(ctx.realStatus === 200); - }); - - it('should get from realStatus ok', () => { - const ctx = app.mockContext(); - ctx.response.status = 302; - ctx.response.realStatus = 404; - assert(ctx.response.realStatus === 404); - assert(ctx.realStatus === 404); - }); - }); - - describe('response.type = type', () => { - it('should remove content-type when type is invalid', () => { - const ctx = app.mockContext(); - ctx.response.type = 'html'; - assert(ctx.response.header['content-type'] === 'text/html; charset=utf-8'); - assert(ctx.response.type === 'text/html'); - - ctx.response.type = 'html-ooooooxxx'; - assert(ctx.response.header['content-type'] === undefined); - assert(ctx.response.type === ''); - }); - }); - }); -}); diff --git a/test/app/extend/response.test.ts b/test/app/extend/response.test.ts new file mode 100644 index 0000000000..8318bf679d --- /dev/null +++ b/test/app/extend/response.test.ts @@ -0,0 +1,115 @@ +import { strict as assert } from 'node:assert'; +import { restore, MockApplication, createApp } from '../../utils.js'; + +describe('test/app/extend/response.test.ts', () => { + afterEach(restore); + + describe('length and type', () => { + let app: MockApplication; + before(() => { + app = createApp('apps/response'); + return app.ready(); + }); + after(() => app.close()); + + it('should get case sensitive header', () => { + return app.httpRequest() + .get('/') + .expect(200) + .expect((res: any) => { + assert.match(JSON.stringify(res.res.rawHeaders), /Content-Type/); + assert.match(JSON.stringify(res.res.rawHeaders), /Content-Length/); + }); + }); + + it('should get {} body', async () => { + const res = await app.httpRequest() + .get('/empty-json') + .expect(200); + assert.deepEqual(res.body, {}); + assert.equal(res.headers['content-length'], '2'); + assert.equal(res.headers['content-type'], 'application/json; charset=utf-8'); + }); + + it('should get body length', () => { + const ctx = app.mockContext(); + ctx.body = null; + const response = ctx.response; + response.remove('content-length'); + assert.equal(response.length, undefined); + + ctx.body = '1'; + response.remove('content-length'); + assert.equal(response.length, 1); + + ctx.body = Buffer.alloc(2); + response.remove('content-length'); + assert.equal(response.length, 2); + + ctx.body = { foo: 'bar' }; + response.remove('content-length'); + assert.equal(response.length, 13); + + ctx.body = '{}'; + response.remove('content-length'); + assert.equal(response.length, 2); + + ctx.body = {}; + response.remove('content-length'); + assert.equal(response.length, 2); + + // mock stream + // ctx.body = { pipe() {} }; + // response.remove('content-length'); + // assert.equal(response.length, undefined); + }); + }); + + describe('test on apps/demo', () => { + let app: MockApplication; + before(() => { + app = createApp('apps/demo'); + return app.ready(); + }); + after(() => app.close()); + + describe('response.realStatus', () => { + it('should get from status ok', () => { + const ctx = app.mockContext(); + ctx.response.status = 200; + assert(ctx.response.realStatus === 200); + assert(ctx.realStatus === 200); + }); + + it('should get from realStatus ok', () => { + const ctx = app.mockContext(); + ctx.response.status = 302; + ctx.response.realStatus = 404; + assert(ctx.response.realStatus === 404); + assert(ctx.realStatus === 404); + }); + }); + + describe('response.type = type', () => { + it('should remove content-type when type is invalid', () => { + let ctx = app.mockContext(); + ctx.response.type = 'html'; + assert.equal(ctx.response.header['content-type'], 'text/html; charset=utf-8'); + assert.equal(ctx.response.type, 'text/html'); + + ctx.response.type = 'xml'; + assert.equal(ctx.response.header['content-type'], 'application/xml'); + assert.equal(ctx.response.type, 'application/xml'); + + ctx = app.mockContext(); + ctx.response.type = 'html-ooooooxxx'; + assert.equal(ctx.response.header['content-type'], undefined); + assert.equal(ctx.response.type, ''); + + ctx.response.type = 'html'; + assert.equal(ctx.response.header['content-type'], 'text/html; charset=utf-8'); + assert.equal(ctx.response.type, 'text/html'); + }); + }); + }); +}); diff --git a/test/app/middleware/body_parser.test.js b/test/app/middleware/body_parser.test.ts similarity index 83% rename from test/app/middleware/body_parser.test.js rename to test/app/middleware/body_parser.test.ts index 04fefcf238..3d5d71f16e 100644 --- a/test/app/middleware/body_parser.test.js +++ b/test/app/middleware/body_parser.test.ts @@ -1,25 +1,21 @@ -const assert = require('assert'); -const querystring = require('querystring'); -const utils = require('../../utils'); - -describe('test/app/middleware/body_parser.test.js', () => { - let app; - let app1; - let csrf; - let cookies; - before(done => { - app = utils.app('apps/body_parser_testapp'); - app.ready(() => { - app.httpRequest() - .get('/test/body_parser/user') - .expect(200, (err, res) => { - assert(!err); - csrf = res.body.csrf || ''; - cookies = res.headers['set-cookie'].join(';'); - assert(csrf); - done(); - }); - }); +import { strict as assert } from 'node:assert'; +import querystring from 'node:querystring'; +import { createApp, MockApplication } from '../../utils.js'; + +describe('test/app/middleware/body_parser.test.ts', () => { + let app: MockApplication; + let app1: MockApplication; + let csrf: string; + let cookies: string; + before(async () => { + app = createApp('apps/body_parser_testapp'); + await app.ready(); + const res = await app.httpRequest() + .get('/test/body_parser/user') + .expect(200); + csrf = res.body.csrf || ''; + cookies = (res.headers['set-cookie'] as any).join(';'); + assert(csrf); }); after(() => app.close()); @@ -109,7 +105,7 @@ describe('test/app/middleware/body_parser.test.js', () => { }); it('should disable body parser', async () => { - app1 = utils.app('apps/body_parser_testapp_disable'); + app1 = createApp('apps/body_parser_testapp_disable'); await app1.ready(); await app1.httpRequest() @@ -119,7 +115,7 @@ describe('test/app/middleware/body_parser.test.js', () => { }); it('should body parser support ignore', async () => { - app1 = utils.app('apps/body_parser_testapp_ignore'); + app1 = createApp('apps/body_parser_testapp_ignore'); await app1.ready(); await app1.httpRequest() @@ -135,7 +131,7 @@ describe('test/app/middleware/body_parser.test.js', () => { }); it('should body parser support match', async () => { - app1 = utils.app('apps/body_parser_testapp_match'); + app1 = createApp('apps/body_parser_testapp_match'); await app1.ready(); await app1.httpRequest() diff --git a/test/app/middleware/meta.test.js b/test/app/middleware/meta.test.ts similarity index 69% rename from test/app/middleware/meta.test.js rename to test/app/middleware/meta.test.ts index 870b0f733b..d877a82048 100644 --- a/test/app/middleware/meta.test.js +++ b/test/app/middleware/meta.test.ts @@ -1,15 +1,16 @@ -const assert = require('assert'); -const mm = require('egg-mock'); -const fs = require('fs/promises'); -const utils = require('../../utils'); +import { strict as assert } from 'node:assert'; +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import { createApp, MockApplication, restore, cluster } from '../../utils.js'; -describe('test/app/middleware/meta.test.js', () => { - afterEach(mm.restore); +describe('test/app/middleware/meta.test.ts', () => { + afterEach(restore); + + let app: MockApplication; describe('default config', () => { - let app; before(() => { - app = utils.app('apps/middlewares'); + app = createApp('apps/middlewares'); return app.ready(); }); after(() => app.close()); @@ -23,9 +24,8 @@ describe('test/app/middleware/meta.test.js', () => { }); describe('config.logger.enablePerformanceTimer = true', () => { - let app; before(() => { - app = utils.app('apps/middlewares-meta-enablePerformanceTimer'); + app = createApp('apps/middlewares-meta-enablePerformanceTimer'); return app.ready(); }); after(() => app.close()); @@ -41,9 +41,8 @@ describe('test/app/middleware/meta.test.js', () => { }); describe('meta.logging = true', () => { - let app; before(() => { - app = utils.app('apps/meta-logging-app'); + app = createApp('apps/meta-logging-app'); return app.ready(); }); after(() => app.close()); @@ -54,16 +53,18 @@ describe('test/app/middleware/meta.test.js', () => { .expect('X-Readtime', /\d+/) .expect('hello world') .expect(200); - if (process.platform === 'win32') await utils.sleep(2000); + if (process.platform === 'win32') { + await scheduler.wait(2000); + } const content = (await fs.readFile(app.coreLogger.options.file, 'utf8')).split('\n').slice(-2, -1)[0]; - assert(content.includes('[meta] request started, host: ')); + assert.match(content, /\[meta] request started, host: /); }); }); describe('cluster start', () => { - let app; + let app: MockApplication; before(() => { - app = utils.cluster('apps/middlewares'); + app = cluster('apps/middlewares'); return app.ready(); }); after(() => app.close()); @@ -72,7 +73,7 @@ describe('test/app/middleware/meta.test.js', () => { return app.httpRequest() .get('/') .expect('X-Readtime', /\d+/) - .expect(res => assert(!res.headers['keep-alive'])) + .expect((res: any) => assert(!res.headers['keep-alive'])) .expect(200); }); diff --git a/test/app/middleware/notfound.test.js b/test/app/middleware/notfound.test.ts similarity index 84% rename from test/app/middleware/notfound.test.js rename to test/app/middleware/notfound.test.ts index 232396214e..ed0bca5a8c 100644 --- a/test/app/middleware/notfound.test.js +++ b/test/app/middleware/notfound.test.ts @@ -1,17 +1,14 @@ -'use strict'; +import { createApp, MockApplication, restore, mm } from '../../utils.js'; -const mm = require('egg-mock'); -const utils = require('../../utils'); - -describe('test/app/middleware/notfound.test.js', () => { - let app; +describe('test/app/middleware/notfound.test.ts', () => { + let app: MockApplication; before(() => { - app = utils.app('apps/notfound'); + app = createApp('apps/notfound'); return app.ready(); }); after(() => app.close()); - afterEach(mm.restore); + afterEach(restore); it('should 302 redirect to 404.html', () => { return app.httpRequest() @@ -25,6 +22,7 @@ describe('test/app/middleware/notfound.test.js', () => { return app.httpRequest() .get('/test/404.json?ctoken=404') .set('Cookie', 'ctoken=404') + .expect('content-type', 'application/json; charset=utf-8') .expect({ message: 'Not Found', }) @@ -50,9 +48,9 @@ describe('test/app/middleware/notfound.test.js', () => { }); describe('config.notfound.pageUrl = "/404"', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/notfound-custom-404'); + app = createApp('apps/notfound-custom-404'); return app.ready(); }); after(() => app.close()); diff --git a/test/app/middleware/override_method.test.js b/test/app/middleware/override_method.test.ts similarity index 84% rename from test/app/middleware/override_method.test.js rename to test/app/middleware/override_method.test.ts index 6037849db9..72da72aa77 100644 --- a/test/app/middleware/override_method.test.js +++ b/test/app/middleware/override_method.test.ts @@ -1,9 +1,9 @@ -const utils = require('../../utils'); +import { createApp, MockApplication } from '../../utils.js'; -describe('test/app/middleware/override_method.test.js', () => { - let app; +describe('test/app/middleware/override_method.test.ts', () => { + let app: MockApplication; before(() => { - app = utils.app('apps/override_method'); + app = createApp('apps/override_method'); return app.ready(); }); after(() => app.close()); diff --git a/test/app/middleware/site_file.test.js b/test/app/middleware/site_file.test.ts similarity index 75% rename from test/app/middleware/site_file.test.js rename to test/app/middleware/site_file.test.ts index 180a628747..bb2ff28dec 100644 --- a/test/app/middleware/site_file.test.js +++ b/test/app/middleware/site_file.test.ts @@ -1,12 +1,10 @@ -'use strict'; +import { strict as assert } from 'node:assert'; +import { createApp, MockApplication } from '../../utils.js'; -const assert = require('assert'); -const utils = require('../../utils'); - -describe('test/app/middleware/site_file.test.js', () => { - let app; +describe('test/app/middleware/site_file.test.ts', () => { + let app: MockApplication; before(() => { - app = utils.app('apps/middlewares'); + app = createApp('apps/middlewares'); return app.ready(); }); after(() => app.close()); @@ -74,9 +72,9 @@ describe('test/app/middleware/site_file.test.js', () => { }); describe('custom favicon', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/favicon'); + app = createApp('apps/favicon'); return app.ready(); }); after(() => app.close()); @@ -87,15 +85,30 @@ describe('test/app/middleware/site_file.test.js', () => { .expect(302) .expect(res => { assert(!res.headers['set-cookie']); - assert(res.headers.location === 'https://eggjs.org/favicon.ico'); + assert.equal(res.headers.location, 'https://eggjs.org/favicon.ico'); }); }); }); + describe('custom favicon with Buffer content', () => { + let app: MockApplication; + before(() => { + app = createApp('apps/favicon-buffer'); + return app.ready(); + }); + after(() => app.close()); + + it('should redirect https://eggjs.org/favicon.ico', () => { + return app.httpRequest() + .get('/favicon.ico') + .expect(200); + }); + }); + describe('custom favicon with function', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/favicon-function'); + app = createApp('apps/favicon-function'); return app.ready(); }); after(() => app.close()); @@ -112,9 +125,9 @@ describe('test/app/middleware/site_file.test.js', () => { }); describe('siteFile.cacheControl = no-store', () => { - let app; + let app: MockApplication; before(() => { - app = utils.app('apps/siteFile-custom-cacheControl'); + app = createApp('apps/siteFile-custom-cacheControl'); return app.ready(); }); after(() => app.close()); 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..7f83b758e0 --- /dev/null +++ b/test/asyncSupport.test.ts @@ -0,0 +1,27 @@ +import { strict as assert } from 'node:assert'; +import { createApp, restore, MockApplication } from './utils.js'; + +describe('test/asyncSupport.test.ts', () => { + // Skip on Windows + if (process.platform === 'win32') return; + + afterEach(restore); + let app: MockApplication; + before(async () => { + app = createApp('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/bench/server.js b/test/bench/server.js index d2d9508dc4..f9cd663b24 100644 --- a/test/bench/server.js +++ b/test/bench/server.js @@ -1,6 +1,6 @@ const http = require('http'); const path = require('path'); -const mock = require('egg-mock'); +const mock = require('@eggjs/mock'); const appName = process.argv[2]; diff --git a/test/fixtures/apps/agent-app-sync/app/router.js b/test/fixtures/apps/agent-app-sync/app/router.js index de9f8095a0..0eeb5f60c6 100644 --- a/test/fixtures/apps/agent-app-sync/app/router.js +++ b/test/fixtures/apps/agent-app-sync/app/router.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = app => { - app.get('/', function*() { + app.get('/', async function() { this.body = this.app.arg; }) }; diff --git a/test/fixtures/apps/agent-app/app/router.js b/test/fixtures/apps/agent-app/app/router.js index 5bd80fd9b8..4985017def 100644 --- a/test/fixtures/apps/agent-app/app/router.js +++ b/test/fixtures/apps/agent-app/app/router.js @@ -1,19 +1,17 @@ -'use strict'; - const sleep = timeout => callback => setTimeout(callback, timeout); module.exports = app => { - app.get('/getData', function*() { - this.body = yield app.mockClient.getData('hello'); + app.get('/getData', async function() { + this.body = await app.mockClient.getData('hello'); }); - app.get('/getDataGenerator', function*() { - this.body = yield app.mockClient.getDataGenerator('hello'); + app.get('/getDataGenerator', async function() { + this.body = await app.mockClient.getDataGenerator('hello'); }); - app.get('/getError', function*() { + app.get('/getError', async function() { try { - yield app.mockClient.getError(); + await app.mockClient.getError(); } catch (err) { this.body = err.message; } @@ -25,10 +23,10 @@ module.exports = app => { }; } - app.get('/sub', function*() { - const first = yield subThunk(); - yield sleep(1000); - const second = yield subThunk(); + app.get('/sub', async function() { + const first = await subThunk(); + await sleep(1000); + const second = await subThunk(); this.body = { foo: app.foo, first, @@ -36,14 +34,14 @@ module.exports = app => { }; }); - app.get('/save', function*() { + app.get('/save', async function() { app.mockClient.saveAsync('hello', 'node'); this.body = 'ok'; }); - app.get('/timeout', function*() { + app.get('/timeout', async function() { try { - yield app.mockClient.getTimeout(); + await app.mockClient.getTimeout(); this.body = 'ok'; } catch (err) { this.body = 'timeout'; diff --git a/test/fixtures/apps/agent-client-app/app/router.js b/test/fixtures/apps/agent-client-app/app/router.js index dea9dee441..63d26db443 100644 --- a/test/fixtures/apps/agent-client-app/app/router.js +++ b/test/fixtures/apps/agent-client-app/app/router.js @@ -13,9 +13,11 @@ module.exports = function(app) { done1(); }); - app.get('/', function*() { - const val = yield cb => app.subClient.subscribe('mock-data', val => { - cb(null, val); + app.get('/', async function() { + const val = await new Promise(resolve => { + app.subClient.subscribe('mock-data', val => { + resolve(val); + }); }); this.body = { @@ -24,9 +26,11 @@ module.exports = function(app) { }; }); - app.get('/not-exist', function*() { - const val = yield cb => app.subClient.subscribe('not-exist-data', val => { - cb(null, val); + app.get('/not-exist', async function() { + const val = await new Promise(resolve => { + app.subClient.subscribe('not-exist-data', val => { + resolve(val); + }); }); this.body = { diff --git a/test/fixtures/apps/agent-throw/app/router.js b/test/fixtures/apps/agent-throw/app/router.js index e15a903b78..eda42edcdd 100644 --- a/test/fixtures/apps/agent-throw/app/router.js +++ b/test/fixtures/apps/agent-throw/app/router.js @@ -1,12 +1,12 @@ 'use strict'; module.exports = app => { - app.get('/agent-throw', function*() { + app.get('/agent-throw', async function() { app.messenger.broadcast('agent-throw'); this.body = 'done'; }); - app.get('/agent-throw-string', function*() { + app.get('/agent-throw-string', async function() { app.messenger.broadcast('agent-throw-string'); this.body = 'done'; }); diff --git a/test/fixtures/apps/aliyun-egg-app/app/controller/home.js b/test/fixtures/apps/aliyun-egg-app/app/controller/home.js index 5d71f1230f..e09c641ffe 100644 --- a/test/fixtures/apps/aliyun-egg-app/app/controller/home.js +++ b/test/fixtures/apps/aliyun-egg-app/app/controller/home.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function*() { +module.exports = async function() { this.body = { 'aliyun-egg-core': !!this.app['aliyun-egg'], 'aliyun-egg-plugin': !!this.app.custom, diff --git a/test/fixtures/apps/app-die-ignore-code/app/router.js b/test/fixtures/apps/app-die-ignore-code/app/router.js index d071d3f5b5..1e0090879b 100644 --- a/test/fixtures/apps/app-die-ignore-code/app/router.js +++ b/test/fixtures/apps/app-die-ignore-code/app/router.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = app => { - app.get('/uncaughtException', function*() { + app.get('/uncaughtException', async function() { setTimeout(() => { const error = new Error('MockError'); error.code = 'EMOCKERROR'; diff --git a/test/fixtures/apps/app-die/app/router.js b/test/fixtures/apps/app-die/app/router.js index 85642afa6a..4631a92947 100644 --- a/test/fixtures/apps/app-die/app/router.js +++ b/test/fixtures/apps/app-die/app/router.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = app => { - app.get('/exit', function*() { + app.get('/exit', async function() { process.exit(1); }); - app.get('/uncaughtException', function*() { + app.get('/uncaughtException', async function() { setTimeout(() => { throw new Error('get uncaughtException'); }, 100); diff --git a/test/fixtures/apps/app-locals-getter/app/router.js b/test/fixtures/apps/app-locals-getter/app/router.js index 011121281c..5b30a84597 100644 --- a/test/fixtures/apps/app-locals-getter/app/router.js +++ b/test/fixtures/apps/app-locals-getter/app/router.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = app => { - app.get('/test', function* () { + app.get('/test', function () { this.app.locals.foo = 'bar'; this.locals.abc = '123'; this.body = { diff --git a/test/fixtures/apps/app-router/app/controller/home.js b/test/fixtures/apps/app-router/app/controller/home.js index 24901f6eec..6179479a06 100644 --- a/test/fixtures/apps/app-router/app/controller/home.js +++ b/test/fixtures/apps/app-router/app/controller/home.js @@ -1,3 +1,3 @@ -module.exports = function* () { +module.exports = async function () { this.body = 'hello'; }; diff --git a/test/fixtures/apps/app-router/config/config.default.js b/test/fixtures/apps/app-router/config/config.default.js index 1c7cb2ea03..be3700e09d 100644 --- a/test/fixtures/apps/app-router/config/config.default.js +++ b/test/fixtures/apps/app-router/config/config.default.js @@ -1,3 +1 @@ -'use strict'; - exports.keys = 'test key'; diff --git a/test/fixtures/apps/app-router/config/plugin.js b/test/fixtures/apps/app-router/config/plugin.js new file mode 100644 index 0000000000..d3697a6580 --- /dev/null +++ b/test/fixtures/apps/app-router/config/plugin.js @@ -0,0 +1,5 @@ +exports.schedule = { + enable: false, +}; + +exports.logrotator = false; diff --git a/test/fixtures/apps/app-server-customized-client-error/app.js b/test/fixtures/apps/app-server-customized-client-error/app.js index 714dc33aff..9bf788e572 100644 --- a/test/fixtures/apps/app-server-customized-client-error/app.js +++ b/test/fixtures/apps/app-server-customized-client-error/app.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = app => { app.on('server', () => { app.serverEmit = true; diff --git a/test/fixtures/apps/app-server-customized-client-error/app/router.js b/test/fixtures/apps/app-server-customized-client-error/app/router.js index b50070496f..bb522314a6 100644 --- a/test/fixtures/apps/app-server-customized-client-error/app/router.js +++ b/test/fixtures/apps/app-server-customized-client-error/app/router.js @@ -1,7 +1,5 @@ -'use strict'; - module.exports = app => { - app.get('/', function* () { + app.get('/', function () { this.body = this.app.serverEmit; }); }; diff --git a/test/fixtures/apps/app-server-customized-client-error/config/config.default.js b/test/fixtures/apps/app-server-customized-client-error/config/config.default.js index a1c4c8d4e8..d29ff82b85 100644 --- a/test/fixtures/apps/app-server-customized-client-error/config/config.default.js +++ b/test/fixtures/apps/app-server-customized-client-error/config/config.default.js @@ -1,11 +1,11 @@ -const { sleep } = require('../../../../utils'); +const { scheduler } = require('node:timers/promises'); exports.keys = 'my keys'; let times = 0; exports.onClientError = async (err, socket, app) => { app.logger.error(err); - await sleep(50); + await scheduler.wait(50); times++; if (times === 2) times = 0; diff --git a/test/fixtures/apps/app-server-timeout/app/router.js b/test/fixtures/apps/app-server-timeout/app/router.js index 5e1dffb09d..4aca9029cf 100644 --- a/test/fixtures/apps/app-server-timeout/app/router.js +++ b/test/fixtures/apps/app-server-timeout/app/router.js @@ -1,4 +1,4 @@ -const { sleep } = require('../../../../utils'); +const { scheduler } = require('node:timers/promises'); module.exports = app => { app.get('/', async ctx => { @@ -6,7 +6,7 @@ module.exports = app => { }); app.get('/timeout', async ctx => { - await sleep(500); + await scheduler.wait(500); ctx.body = 'timeout'; }); }; diff --git a/test/fixtures/apps/app-server-with-hostname/app/router.js b/test/fixtures/apps/app-server-with-hostname/app/router.js index fe7574110b..2a7b6673a4 100644 --- a/test/fixtures/apps/app-server-with-hostname/app/router.js +++ b/test/fixtures/apps/app-server-with-hostname/app/router.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = app => { - app.get('/', function* () { + app.get('/', function () { this.body = 'done'; }); }; diff --git a/test/fixtures/apps/app-server/app/router.js b/test/fixtures/apps/app-server/app/router.js index 2e7e66a3ed..bb522314a6 100644 --- a/test/fixtures/apps/app-server/app/router.js +++ b/test/fixtures/apps/app-server/app/router.js @@ -1,5 +1,5 @@ module.exports = app => { - app.get('/', function* () { + app.get('/', function () { this.body = this.app.serverEmit; }); }; diff --git a/test/fixtures/apps/app-throw/app/router.js b/test/fixtures/apps/app-throw/app/router.js index f8dfc7a998..32889a48a0 100644 --- a/test/fixtures/apps/app-throw/app/router.js +++ b/test/fixtures/apps/app-throw/app/router.js @@ -1,14 +1,14 @@ 'use strict'; module.exports = app => { - app.get('/throw', function* () { + app.get('/throw', function () { this.body = 'foo'; setTimeout(() => { a.b = c; }, 1); }); - app.get('/throw-error-setter', function* () { + app.get('/throw-error-setter', function () { this.body = 'foo'; setTimeout(() => { const err = new Error('abc'); @@ -20,21 +20,21 @@ module.exports = app => { }, 1); }); - app.get('/throw-unhandledRejection', function* () { + app.get('/throw-unhandledRejection', function () { this.body = 'foo'; new Promise((resolve, reject) => { reject(new Error('foo reject error')); }); }); - app.get('/throw-unhandledRejection-string', function* () { + app.get('/throw-unhandledRejection-string', function () { this.body = 'foo'; new Promise((resolve, reject) => { reject('foo reject string error'); }); }); - app.get('/throw-unhandledRejection-obj', function* () { + app.get('/throw-unhandledRejection-obj', function () { this.body = 'foo'; new Promise((resolve, reject) => { const err = { diff --git a/test/fixtures/apps/app-ts-type-check/xiandan.d.ts b/test/fixtures/apps/app-ts-type-check/xiandan.d.ts index 8bd94303c8..65e7d5819a 100644 --- a/test/fixtures/apps/app-ts-type-check/xiandan.d.ts +++ b/test/fixtures/apps/app-ts-type-check/xiandan.d.ts @@ -1,23 +1,23 @@ -import * as Egg from 'yadan'; +// import * as Egg from 'yadan'; -declare module 'egg' { - interface Application { - fromXiandan(): Promise; - } +// declare module 'egg' { +// interface Application { +// fromXiandan(): Promise; +// } - interface Context { - fromXiandan(): Promise; - } +// interface Context { +// fromXiandan(): Promise; +// } - interface EggAppConfig { - XiandanType: string; - } -} +// interface EggAppConfig { +// XiandanType: string; +// } +// } -declare module 'xiandan' { - class XiandanApplication extends Application { - superXiandan(): Promise; - } -} +// declare module 'xiandan' { +// class XiandanApplication extends Application { +// superXiandan(): Promise; +// } +// } -export = Egg; \ No newline at end of file +// export = Egg; diff --git a/test/fixtures/apps/app-ts-type-check/yadan.d.ts b/test/fixtures/apps/app-ts-type-check/yadan.d.ts index 16693e7600..98a0cc775a 100644 --- a/test/fixtures/apps/app-ts-type-check/yadan.d.ts +++ b/test/fixtures/apps/app-ts-type-check/yadan.d.ts @@ -1,23 +1,23 @@ -import * as Egg from 'egg'; +// import * as Egg from 'egg'; -declare module 'egg' { - interface Application { - fromYadan(): Promise; - } +// declare module 'egg' { +// interface Application { +// fromYadan(): Promise; +// } - interface Context { - fromYadan(): Promise; - } +// interface Context { +// fromYadan(): Promise; +// } - interface EggAppConfig { - yadanType: string; - } -} +// interface EggAppConfig { +// yadanType: string; +// } +// } -declare module 'yadan' { - class YadanApplication extends Application { - superYadan(): Promise; - } -} +// declare module 'yadan' { +// class YadanApplication extends Application { +// superYadan(): Promise; +// } +// } -export = Egg; +// export = Egg; diff --git a/test/fixtures/apps/async-app/app.js b/test/fixtures/apps/async-app/app.js index 83f0c700ab..bbda94fe6c 100644 --- a/test/fixtures/apps/async-app/app.js +++ b/test/fixtures/apps/async-app/app.js @@ -1,10 +1,11 @@ -'use strict'; - module.exports = app => { app.beforeStart(async () => { await Promise.resolve(); + app.beforeStartExecuted = true; + }); + + app.ready(async () => { await app.runSchedule('async'); - app.beforeStartExectuted = true; }); app.beforeClose(async () => { diff --git a/test/fixtures/apps/async-app/app/router.js b/test/fixtures/apps/async-app/app/router.js index fd7b5af97c..3b661d1b7b 100644 --- a/test/fixtures/apps/async-app/app/router.js +++ b/test/fixtures/apps/async-app/app/router.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = app => { app.get('/api', app.middlewares.router(), 'api.index'); }; diff --git a/test/fixtures/apps/async-app/app/schedule/async.js b/test/fixtures/apps/async-app/app/schedule/async.js index 33509e8fd8..04863f1681 100644 --- a/test/fixtures/apps/async-app/app/schedule/async.js +++ b/test/fixtures/apps/async-app/app/schedule/async.js @@ -1,5 +1,3 @@ -'use strict'; - exports.schedule = { type: 'worker', interval: 1000000, diff --git a/test/fixtures/apps/body_parser_testapp/app/router.js b/test/fixtures/apps/body_parser_testapp/app/router.js index 546a60ccc9..5a58563ba4 100644 --- a/test/fixtures/apps/body_parser_testapp/app/router.js +++ b/test/fixtures/apps/body_parser_testapp/app/router.js @@ -1,20 +1,20 @@ module.exports = app => { - app.get('/test/body_parser/user', function* () { + app.get('/test/body_parser/user', function () { this.body = { url: this.url, csrf: this.csrf }; }); - app.post('/test/body_parser/user', function* () { + app.post('/test/body_parser/user', function () { this.logger.info('request body %s', this.request.body); this.body = this.request.body; }); - app.post('/test/body_parser/foo.json', function* () { + app.post('/test/body_parser/foo.json', function () { this.body = this.request.body; }); - app.post('/test/body_parser/form.json', function* () { + app.post('/test/body_parser/form.json', function () { this.body = this.request.body; }); }; diff --git a/test/fixtures/apps/body_parser_testapp_disable/app/router.js b/test/fixtures/apps/body_parser_testapp_disable/app/router.js index 276433c1f8..7b687f104a 100644 --- a/test/fixtures/apps/body_parser_testapp_disable/app/router.js +++ b/test/fixtures/apps/body_parser_testapp_disable/app/router.js @@ -1,19 +1,19 @@ module.exports = app => { - app.get('/test/body_parser/user', function* () { + app.get('/test/body_parser/user', function () { this.body = { url: this.url, csrf: this.csrf }; }); - app.post('/test/body_parser/user', function* () { + app.post('/test/body_parser/user', function () { this.body = this.request.body; }); - app.post('/test/body_parser/foo.json', function* () { + app.post('/test/body_parser/foo.json', function () { this.body = this.request.body; }); - app.post('/test/body_parser/form.json', function* () { + app.post('/test/body_parser/form.json', function () { this.body = this.request.body; }); }; diff --git a/test/fixtures/apps/body_parser_testapp_ignore/app/router.js b/test/fixtures/apps/body_parser_testapp_ignore/app/router.js index 276433c1f8..7b687f104a 100644 --- a/test/fixtures/apps/body_parser_testapp_ignore/app/router.js +++ b/test/fixtures/apps/body_parser_testapp_ignore/app/router.js @@ -1,19 +1,19 @@ module.exports = app => { - app.get('/test/body_parser/user', function* () { + app.get('/test/body_parser/user', function () { this.body = { url: this.url, csrf: this.csrf }; }); - app.post('/test/body_parser/user', function* () { + app.post('/test/body_parser/user', function () { this.body = this.request.body; }); - app.post('/test/body_parser/foo.json', function* () { + app.post('/test/body_parser/foo.json', function () { this.body = this.request.body; }); - app.post('/test/body_parser/form.json', function* () { + app.post('/test/body_parser/form.json', function () { this.body = this.request.body; }); }; diff --git a/test/fixtures/apps/body_parser_testapp_match/app/router.js b/test/fixtures/apps/body_parser_testapp_match/app/router.js index 276433c1f8..7b687f104a 100644 --- a/test/fixtures/apps/body_parser_testapp_match/app/router.js +++ b/test/fixtures/apps/body_parser_testapp_match/app/router.js @@ -1,19 +1,19 @@ module.exports = app => { - app.get('/test/body_parser/user', function* () { + app.get('/test/body_parser/user', function () { this.body = { url: this.url, csrf: this.csrf }; }); - app.post('/test/body_parser/user', function* () { + app.post('/test/body_parser/user', function () { this.body = this.request.body; }); - app.post('/test/body_parser/foo.json', function* () { + app.post('/test/body_parser/foo.json', function () { this.body = this.request.body; }); - app.post('/test/body_parser/form.json', function* () { + app.post('/test/body_parser/form.json', function () { this.body = this.request.body; }); }; diff --git a/test/fixtures/apps/cluster_mod_app/agent.js b/test/fixtures/apps/cluster_mod_app/agent.js index c10b041524..51f7e9210a 100644 --- a/test/fixtures/apps/cluster_mod_app/agent.js +++ b/test/fixtures/apps/cluster_mod_app/agent.js @@ -13,9 +13,9 @@ module.exports = function(agent) { cluster: agent.cluster, }); - agent.beforeStart(function*() { - yield agent.registryClient.ready(); - yield agent.apiClient.ready(); - yield agent.apiClient2.ready(); + agent.beforeStart(async function() { + await agent.registryClient.ready(); + await agent.apiClient.ready(); + await agent.apiClient2.ready(); }); }; diff --git a/test/fixtures/apps/cluster_mod_app/app.js b/test/fixtures/apps/cluster_mod_app/app.js index 46b1461cb2..0777e1d9e9 100644 --- a/test/fixtures/apps/cluster_mod_app/app.js +++ b/test/fixtures/apps/cluster_mod_app/app.js @@ -17,9 +17,9 @@ module.exports = function(app) { app.apiClient = new ApiClient({ cluster }); app.apiClient2 = new ApiClient2({ cluster }); - app.beforeStart(function*() { - yield app.registryClient.ready(); - yield app.apiClient.ready(); - yield app.apiClient2.ready(); + app.beforeStart(async function() { + await app.registryClient.ready(); + await app.apiClient.ready(); + await app.apiClient2.ready(); }); }; diff --git a/test/fixtures/apps/cluster_mod_app/app/controller/home.js b/test/fixtures/apps/cluster_mod_app/app/controller/home.js index d28a8dfa6f..de6408807b 100644 --- a/test/fixtures/apps/cluster_mod_app/app/controller/home.js +++ b/test/fixtures/apps/cluster_mod_app/app/controller/home.js @@ -1,18 +1,18 @@ 'use strict'; -exports.index = function*() { +exports.index = async function() { this.body = 'hi cluster'; }; -exports.getClusterPort = function*() { +exports.getClusterPort = async function() { this.body = this.app._options.clusterPort; }; -exports.getHosts = function*() { +exports.getHosts = async function() { this.body = this.app.val && this.app.val.map(url => url.host).join(','); }; -exports.publish = function*() { +exports.publish = async function() { const val = this.request.body.value; this.app.registryClient.publish({ dataId: 'demo.DemoService', diff --git a/test/fixtures/apps/csrf-disable/app/controller/api.js b/test/fixtures/apps/csrf-disable/app/controller/api.js index 8a8cb142b6..c37001a9ba 100644 --- a/test/fixtures/apps/csrf-disable/app/controller/api.js +++ b/test/fixtures/apps/csrf-disable/app/controller/api.js @@ -1,6 +1,6 @@ 'use strict'; -exports.user = function* () { +exports.user = function () { this.body = { url: this.url, name: this.request.body.name, diff --git a/test/fixtures/apps/csrf-enable/app/controller/api.js b/test/fixtures/apps/csrf-enable/app/controller/api.js index 4cff137291..ffd6b18505 100644 --- a/test/fixtures/apps/csrf-enable/app/controller/api.js +++ b/test/fixtures/apps/csrf-enable/app/controller/api.js @@ -1,6 +1,6 @@ 'use strict'; -exports.user = function* () { +exports.user = function () { this.body = { url: this.url, name: this.query.name, diff --git a/test/fixtures/apps/csrf-ignore/app/controller/api.js b/test/fixtures/apps/csrf-ignore/app/controller/api.js index 8a8cb142b6..c37001a9ba 100644 --- a/test/fixtures/apps/csrf-ignore/app/controller/api.js +++ b/test/fixtures/apps/csrf-ignore/app/controller/api.js @@ -1,6 +1,6 @@ 'use strict'; -exports.user = function* () { +exports.user = function () { this.body = { url: this.url, name: this.request.body.name, diff --git a/test/fixtures/apps/custom-context-getlogger/app/controller/home.js b/test/fixtures/apps/custom-context-getlogger/app/controller/home.js index 6e9c3f6ce3..83ca1e4b0e 100644 --- a/test/fixtures/apps/custom-context-getlogger/app/controller/home.js +++ b/test/fixtures/apps/custom-context-getlogger/app/controller/home.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function* () { +module.exports = async function () { const logger = this.getLogger('foo'); logger.info('hello'); this.body = 'work, logger: ' + (logger ? 'exists' : 'not exists'); diff --git a/test/fixtures/apps/custom-context-getlogger/app/extend/context.js b/test/fixtures/apps/custom-context-getlogger/app/extend/context.js index 264d7e950f..ad504d402c 100644 --- a/test/fixtures/apps/custom-context-getlogger/app/extend/context.js +++ b/test/fixtures/apps/custom-context-getlogger/app/extend/context.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = { getLogger(name) { console.log('get custom %s logger', name); diff --git a/test/fixtures/apps/custom-env-app/app/router.js b/test/fixtures/apps/custom-env-app/app/router.js index a4e9705aea..d378caa868 100644 --- a/test/fixtures/apps/custom-env-app/app/router.js +++ b/test/fixtures/apps/custom-env-app/app/router.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = app => { - app.get('/', function*() { + app.get('/', async function() { this.body = { env: this.app.config.env, }; diff --git a/test/fixtures/apps/custom-framework-demo/app/controller/hello.js b/test/fixtures/apps/custom-framework-demo/app/controller/hello.js index 1c9d1b71cb..28718d80d8 100644 --- a/test/fixtures/apps/custom-framework-demo/app/controller/hello.js +++ b/test/fixtures/apps/custom-framework-demo/app/controller/hello.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function*() { +module.exports = async function() { this.cookies.set('hi', 'foo'); this.body = 'hello'; }; diff --git a/test/fixtures/apps/custom-framework-demo/app/controller/home.js b/test/fixtures/apps/custom-framework-demo/app/controller/home.js index fb335b81ac..74ad87e0f5 100644 --- a/test/fixtures/apps/custom-framework-demo/app/controller/home.js +++ b/test/fixtures/apps/custom-framework-demo/app/controller/home.js @@ -1,4 +1,4 @@ -module.exports = function* () { +module.exports = async function () { this.body = { workerTitle: process.title }; diff --git a/test/fixtures/apps/custom-framework-demo/app/controller/ip.js b/test/fixtures/apps/custom-framework-demo/app/controller/ip.js index 8b3fe6df6d..fefbf0e5ed 100644 --- a/test/fixtures/apps/custom-framework-demo/app/controller/ip.js +++ b/test/fixtures/apps/custom-framework-demo/app/controller/ip.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function* () { +module.exports = async function () { if (this.query.set_ip) { this.ip = this.query.set_ip; } diff --git a/test/fixtures/apps/custom-framework-demo/app/controller/logger.js b/test/fixtures/apps/custom-framework-demo/app/controller/logger.js index d56ffddb83..24bb1992dc 100644 --- a/test/fixtures/apps/custom-framework-demo/app/controller/logger.js +++ b/test/fixtures/apps/custom-framework-demo/app/controller/logger.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function*() { +module.exports = async function() { const message = this.query.message; this.logger.debug('debug %s', message); diff --git a/test/fixtures/apps/custom-framework-demo/app/router.js b/test/fixtures/apps/custom-framework-demo/app/router.js index 80a81c057e..689b441275 100644 --- a/test/fixtures/apps/custom-framework-demo/app/router.js +++ b/test/fixtures/apps/custom-framework-demo/app/router.js @@ -2,11 +2,11 @@ module.exports = app => { app.get('home', '/', 'home'); app.get('/hello', app.controller.hello); app.get('/logger', app.controller.logger); - app.get('/protocol', function*() { + app.get('/protocol', async function() { this.body = this.protocol; }); - app.get('/user.json', app.jsonp(), function*() { + app.get('/user.json', app.jsonp(), async function() { this.body = { name: 'fengmk2' }; }); app.get('/ip', app.controller.ip); diff --git a/test/fixtures/apps/demo/app.js b/test/fixtures/apps/demo/app.js index 72d92f6cd4..2f4c36fb4c 100644 --- a/test/fixtures/apps/demo/app.js +++ b/test/fixtures/apps/demo/app.js @@ -1,7 +1,4 @@ -'use strict'; -const mm = require('egg-mock'); -const assert = require('assert'); -const utils = require('../../../utils'); +const { mm } = require('@eggjs/mock'); class DemoAppTest { diff --git a/test/fixtures/apps/demo/app/controller/foo.js b/test/fixtures/apps/demo/app/controller/foo.js index d6f9cd47a1..32bf299aad 100644 --- a/test/fixtures/apps/demo/app/controller/foo.js +++ b/test/fixtures/apps/demo/app/controller/foo.js @@ -2,7 +2,7 @@ module.exports = app => { return class Foo extends app.Controller { - * bar() { + async bar() { this.ctx.body = 'this is bar!'; } }; diff --git a/test/fixtures/apps/demo/app/controller/hello.js b/test/fixtures/apps/demo/app/controller/hello.js index 1c9d1b71cb..28718d80d8 100644 --- a/test/fixtures/apps/demo/app/controller/hello.js +++ b/test/fixtures/apps/demo/app/controller/hello.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function*() { +module.exports = async function() { this.cookies.set('hi', 'foo'); this.body = 'hello'; }; diff --git a/test/fixtures/apps/demo/app/controller/home.js b/test/fixtures/apps/demo/app/controller/home.js index fb335b81ac..74ad87e0f5 100644 --- a/test/fixtures/apps/demo/app/controller/home.js +++ b/test/fixtures/apps/demo/app/controller/home.js @@ -1,4 +1,4 @@ -module.exports = function* () { +module.exports = async function () { this.body = { workerTitle: process.title }; diff --git a/test/fixtures/apps/demo/app/controller/ip.js b/test/fixtures/apps/demo/app/controller/ip.js index 8b3fe6df6d..fefbf0e5ed 100644 --- a/test/fixtures/apps/demo/app/controller/ip.js +++ b/test/fixtures/apps/demo/app/controller/ip.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function* () { +module.exports = async function () { if (this.query.set_ip) { this.ip = this.query.set_ip; } diff --git a/test/fixtures/apps/demo/app/controller/logger.js b/test/fixtures/apps/demo/app/controller/logger.js index d56ffddb83..e52d4a1a8e 100644 --- a/test/fixtures/apps/demo/app/controller/logger.js +++ b/test/fixtures/apps/demo/app/controller/logger.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function*() { +module.exports = async function() { const message = this.query.message; this.logger.debug('debug %s', message); diff --git a/test/fixtures/apps/demo/app/controller/obj.js b/test/fixtures/apps/demo/app/controller/obj.js index 260b80c6e6..d9b64a980d 100644 --- a/test/fixtures/apps/demo/app/controller/obj.js +++ b/test/fixtures/apps/demo/app/controller/obj.js @@ -1,17 +1,15 @@ -'use strict'; - -module.exports = app => { +module.exports = () => { return { - * bar() { + async bar() { this.ctx.body = 'this is obj bar!'; }, - * error() { + async error() { aaa; }, subObj: { - * hello() { + async hello() { this.ctx.body = 'this is subObj hello!'; }, }, diff --git a/test/fixtures/apps/demo/app/controller/obj2.js b/test/fixtures/apps/demo/app/controller/obj2.js index 9817c45eee..1b8074e1d2 100644 --- a/test/fixtures/apps/demo/app/controller/obj2.js +++ b/test/fixtures/apps/demo/app/controller/obj2.js @@ -1,17 +1,15 @@ -'use strict'; - module.exports = { - * bar() { + async bar() { this.ctx.body = 'this is obj bar!'; }, subObj: { - * hello() { + async hello() { this.ctx.body = 'this is subObj hello!'; }, subSubObj: { - * hello() { + async hello() { this.ctx.body = 'this is subSubObj hello!'; }, }, diff --git a/test/fixtures/apps/demo/app/router.js b/test/fixtures/apps/demo/app/router.js index 80a81c057e..689b441275 100644 --- a/test/fixtures/apps/demo/app/router.js +++ b/test/fixtures/apps/demo/app/router.js @@ -2,11 +2,11 @@ module.exports = app => { app.get('home', '/', 'home'); app.get('/hello', app.controller.hello); app.get('/logger', app.controller.logger); - app.get('/protocol', function*() { + app.get('/protocol', async function() { this.body = this.protocol; }); - app.get('/user.json', app.jsonp(), function*() { + app.get('/user.json', app.jsonp(), async function() { this.body = { name: 'fengmk2' }; }); app.get('/ip', app.controller.ip); diff --git a/test/fixtures/apps/development/app/router.js b/test/fixtures/apps/development/app/router.js index e0cefa5068..ce2798c6e1 100644 --- a/test/fixtures/apps/development/app/router.js +++ b/test/fixtures/apps/development/app/router.js @@ -1,11 +1,11 @@ 'use strict'; module.exports = app => { - app.get('/foo.js', function* () { + app.get('/foo.js', function () { this.body = 'foo.js'; }); - app.get('/foo', function* () { + app.get('/foo', function () { this.body = 'foo'; }); }; diff --git a/test/fixtures/apps/dnscache_httpclient/app/controller/home.js b/test/fixtures/apps/dnscache_httpclient/app/controller/home.js index ecf3f40a26..6b00048c22 100644 --- a/test/fixtures/apps/dnscache_httpclient/app/controller/home.js +++ b/test/fixtures/apps/dnscache_httpclient/app/controller/home.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function* () { +module.exports = async function () { let args; if (this.query.host) { args = {}; @@ -13,7 +13,7 @@ module.exports = function* () { if (this.query.disableDNSCache) { args = { enableDNSCache: false }; } - const result = yield this.curl(this.query.url, args); + const result = await this.curl(this.query.url, args); this.status = result.status; this.set(result.headers); this.body = result.data; diff --git a/test/fixtures/apps/dumpconfig-circular/app.js b/test/fixtures/apps/dumpconfig-circular/app.js index 00d429f10a..fa1bc8ad07 100644 --- a/test/fixtures/apps/dumpconfig-circular/app.js +++ b/test/fixtures/apps/dumpconfig-circular/app.js @@ -1,8 +1,8 @@ -const { sleep } = require('../../../utils'); +const { scheduler } = require('node:timers/promises'); module.exports = app => { - app.beforeStart(function*() { - yield sleep(500); + app.beforeStart(async function() { + await scheduler.wait(500); app.config.foo.push(app.config.foo); }); }; diff --git a/test/fixtures/apps/dumpconfig/app.js b/test/fixtures/apps/dumpconfig/app.js index ada4783b55..e6b6fbf5bf 100644 --- a/test/fixtures/apps/dumpconfig/app.js +++ b/test/fixtures/apps/dumpconfig/app.js @@ -1,7 +1,7 @@ const fs = require('fs'); const path = require('path'); const assert = require('assert'); -const { sleep } = require('../../../utils'); +const { scheduler } = require('node:timers/promises'); function readJSON(p) { return JSON.parse(fs.readFileSync(p)); @@ -12,14 +12,14 @@ module.exports = app => { let json; app.config.dynamic = 1; - app.beforeStart(function*() { + app.beforeStart(async function() { // dumpConfig() dynamically json = readJSON(path.join(baseDir, 'run/application_config.json')); assert(json.config.dynamic === 1, 'should dump in config'); json = readJSON(path.join(baseDir, 'run/agent_config.json')); assert(json.config.dynamic === 0, 'should dump in config'); - yield sleep(2000); + await scheduler.wait(2000); app.config.dynamic = 2; }); }; diff --git a/test/fixtures/apps/encrypt-cookies/app/controller/home.js b/test/fixtures/apps/encrypt-cookies/app/controller/home.js index 6158ea637d..e287037dd6 100644 --- a/test/fixtures/apps/encrypt-cookies/app/controller/home.js +++ b/test/fixtures/apps/encrypt-cookies/app/controller/home.js @@ -1,4 +1,4 @@ -module.exports = function* () { +module.exports = async function () { var encrypt = this.cookies.get('foo', { encrypt: true }); diff --git a/test/fixtures/apps/favicon-buffer/app/controller/home.js b/test/fixtures/apps/favicon-buffer/app/controller/home.js new file mode 100644 index 0000000000..962f8fcba9 --- /dev/null +++ b/test/fixtures/apps/favicon-buffer/app/controller/home.js @@ -0,0 +1,3 @@ +module.exports = async function() { + this.body = 'home'; +}; diff --git a/test/fixtures/apps/favicon-buffer/app/router.js b/test/fixtures/apps/favicon-buffer/app/router.js new file mode 100644 index 0000000000..fa70390f6a --- /dev/null +++ b/test/fixtures/apps/favicon-buffer/app/router.js @@ -0,0 +1,3 @@ +module.exports = app => { + app.get('/', app.controller.home); +}; diff --git a/test/fixtures/apps/favicon-buffer/config/config.default.js b/test/fixtures/apps/favicon-buffer/config/config.default.js new file mode 100644 index 0000000000..908c473727 --- /dev/null +++ b/test/fixtures/apps/favicon-buffer/config/config.default.js @@ -0,0 +1,5 @@ +exports.siteFile = { + '/favicon.ico': Buffer.from('https://eggjs.org/favicon.ico'), +} + +exports.keys = 'foo'; diff --git a/test/fixtures/apps/favicon-buffer/package.json b/test/fixtures/apps/favicon-buffer/package.json new file mode 100644 index 0000000000..258f5343a2 --- /dev/null +++ b/test/fixtures/apps/favicon-buffer/package.json @@ -0,0 +1,3 @@ +{ + "name": "favicon-buffer" +} diff --git a/test/fixtures/apps/favicon/app/controller/home.js b/test/fixtures/apps/favicon/app/controller/home.js index ac412a98d8..1a71ad2e1d 100644 --- a/test/fixtures/apps/favicon/app/controller/home.js +++ b/test/fixtures/apps/favicon/app/controller/home.js @@ -1,5 +1,3 @@ -'use strict'; - -module.exports = function*() { +module.exports = async function () { this.body = 'home'; }; diff --git a/test/fixtures/apps/get-logger/app/router.js b/test/fixtures/apps/get-logger/app/router.js index 1903c0ec42..0a688166df 100644 --- a/test/fixtures/apps/get-logger/app/router.js +++ b/test/fixtures/apps/get-logger/app/router.js @@ -1,12 +1,10 @@ -'use strict'; - module.exports = function(app) { - app.get('/logger', function* () { + app.get('/logger', function () { this.getLogger('aLogger').info('aaa'); this.body = 'done'; }); - app.get('/noExistLogger', function* () { + app.get('/noExistLogger', function () { this.body = this.app.getLogger('noexist') + ''; }); }; diff --git a/test/fixtures/apps/helper/app/controller/home.js b/test/fixtures/apps/helper/app/controller/home.js index 62d57de545..ad3e5e0283 100644 --- a/test/fixtures/apps/helper/app/controller/home.js +++ b/test/fixtures/apps/helper/app/controller/home.js @@ -1,3 +1,3 @@ -module.exports = function* () { +module.exports = async function () { this.body = 'hello home'; }; diff --git a/test/fixtures/apps/helper/app/router.js b/test/fixtures/apps/helper/app/router.js index d9c4640fa9..36df292ce9 100644 --- a/test/fixtures/apps/helper/app/router.js +++ b/test/fixtures/apps/helper/app/router.js @@ -1,33 +1,33 @@ module.exports = app => { app.get('home', '/home', 'home'); - app.get('/pathFor', function* () { + app.get('/pathFor', function () { this.body = this.helper.pathFor('home', this.query); }); - app.get('/urlFor', function* () { + app.get('/urlFor', function () { this.body = this.helper.urlFor('home', this.query); }); - app.get('/escape', function* () { + app.get('/escape', function () { this.body = this.helper.escape('