diff --git a/.autod.conf.js b/.autod.conf.js deleted file mode 100644 index b68b6be..0000000 --- a/.autod.conf.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -module.exports = { - write: true, - prefix: '^', - test: [ - 'test', - 'benchmark', - ], - dep: [ - ], - devdep: [ - 'egg', - 'egg-ci', - 'egg-bin', - 'autod', - 'eslint', - 'eslint-config-egg', - 'webstorm-disable-index', - ], - exclude: [ - './test/fixtures', - './dist', - ], -}; diff --git a/.eslintignore b/.eslintignore index a24e501..8cc505f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ -test/fixtures coverage +test/fixtures/ts/app/controller/home.js +test/fixtures/ts-pkg/app/controller/home.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index c799fe5..9bcdb46 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "eslint-config-egg" + "extends": [ + "eslint-config-egg/typescript", + "eslint-config-egg/lib/rules/enforce-node-prefix" + ] } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 48f9944..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,24 +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 - diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 0000000..65e67b0 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + Job: + name: Node.js + uses: node-modules/github-actions/.github/workflows/node-test.yml@master + with: + version: '18.19.0, 18, 20, 22' + os: 'ubuntu-latest, macos-latest' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml new file mode 100644 index 0000000..bac3fac --- /dev/null +++ b/.github/workflows/pkg.pr.new.yml @@ -0,0 +1,23 @@ +name: Publish Any Commit +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run prepublishOnly --if-present + + - run: npx pkg-pr-new publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a2bf04a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,13 @@ +name: Release + +on: + push: + branches: [ master ] + +jobs: + release: + name: Node.js + uses: eggjs/github-actions/.github/workflows/node-release.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} diff --git a/.gitignore b/.gitignore index f18c766..8189f41 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ logs/ npm-debug.log -node_modules/ +node_modules coverage/ .idea/ run/ .DS_Store *.swp -!test/fixtures/example/node_modules - +test/fixtures/ts/app/controller/home.js +test/fixtures/ts-pkg/app/controller/home.js +!test/fixtures/**/node_modules +package-lock.json +.package-lock.json +.tshy* +.eslintcache +dist diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 47cc542..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -sudo: false -language: node_js -node_js: - - '6' - - '8' -install: - - npm i npminstall && npminstall -script: - - npm run ci -after_script: - - npminstall codecov && codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4ed0672 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,289 @@ +# Changelog + +## [4.0.0](https://github.com/eggjs/scripts/compare/v3.1.0...v4.0.0) (2024-12-31) + + +### ⚠ BREAKING CHANGES + +* drop Node.js < 18.19.0 support + +part of https://github.com/eggjs/egg/issues/3644 + +https://github.com/eggjs/egg/issues/5257 + + + +## Summary by CodeRabbit + +## Release Notes + +- **New Features** + - Added support for ECMAScript modules (ESM). + - Enhanced CLI with more robust start and stop commands. + - Improved TypeScript integration and type safety. + - Introduced new commands for stopping an Egg.js server application. + - Added new configuration options for logging and process management. + +- **Improvements** + - Updated package configuration for better modularity. + - Modernized test infrastructure with TypeScript support. + - Refined error handling and logging mechanisms. + - Enhanced process management capabilities. +- Improved documentation with updated installation instructions and +usage examples. + +- **Breaking Changes** + - Renamed package from `egg-scripts` to `@eggjs/scripts`. + - Requires Node.js version 18.19.0 or higher. + - Significant changes to project structure and module exports. + +- **Bug Fixes** + - Improved process management for server start and stop operations. + - Enhanced cross-platform compatibility. +- Fixed issues with asynchronous route handlers in various applications. + + +### Features + +* support cjs and esm both by tshy ([#63](https://github.com/eggjs/scripts/issues/63)) ([d9d0bc6](https://github.com/eggjs/scripts/commit/d9d0bc65aefd1d73be2c40b86366af566cf471c1)) + +## [3.1.0](https://github.com/eggjs/egg-scripts/compare/v3.0.1...v3.1.0) (2024-12-10) + + +### Features + +* use runscript v2 ([#61](https://github.com/eggjs/egg-scripts/issues/61)) ([ebbb7f6](https://github.com/eggjs/egg-scripts/commit/ebbb7f60446a2bf5ec8eaac40c85c6224dd91c9d)) + +## [3.0.1](https://github.com/eggjs/egg-scripts/compare/v3.0.0...v3.0.1) (2024-05-11) + + +### Bug Fixes + +* head 100 stderr when startup failed ([#59](https://github.com/eggjs/egg-scripts/issues/59)) ([7f2cecf](https://github.com/eggjs/egg-scripts/commit/7f2cecfeb68f07e9b69871f77b66f8a221c51b90)) + +## [3.0.0](https://github.com/eggjs/egg-scripts/compare/v2.17.0...v3.0.0) (2024-02-19) + + +### ⚠ BREAKING CHANGES + +* drop Node.js 14 support + +### Features + +* support configure egg.revert in package.json ([#58](https://github.com/eggjs/egg-scripts/issues/58)) ([a294691](https://github.com/eggjs/egg-scripts/commit/a29469134293a9dec3a7dd5cf6ce71810e913498)) + + +--- + + +2.17.0 / 2022-04-28 +================== + +**features** + * [[`47f8e82`](http://github.com/eggjs/egg-scripts/commit/47f8e823e01b74028bf8dee7123fc3f9469fb3b6)] - feat: eggScriptsConfig support node-options (#54) (TZ | 天猪 <>) + +2.16.0 / 2022-03-27 +================== + +**features** + * [[`bb1ba0a`](http://github.com/eggjs/egg-scripts/commit/bb1ba0a665cab9530639d98f38b76c3c72176f76)] - feat: --trace-warnings (#53) (mansonchor.github.com <>) + +2.15.3 / 2022-03-08 +================== + +**fixes** + * [[`ef5496d`](http://github.com/eggjs/egg-scripts/commit/ef5496d1838a508a859cd5d77886098d7de8fec5)] - fix: ps-cmd result may be truncated (#52) (W <>) + +**others** + * [[`be89f9d`](http://github.com/eggjs/egg-scripts/commit/be89f9d6bb88810ffa3237deab9e4e0d9c4000c2)] - docs(doc): 修改readme文档中的stop脚本处的描述,并增加示例. (#51) (shuidian <<18842643145@163.com>>) + +2.15.2 / 2021-11-17 +================== + +**fixes** + * [[`b122d86`](http://github.com/eggjs/egg-scripts/commit/b122d86d300df4018291d8f8d8e98ab813048f67)] - fix: sourcemap default value should respect eggScriptConfig (#50) (killa <>) + +**others** + * [[`78c3284`](http://github.com/eggjs/egg-scripts/commit/78c3284cb68748f4487141f5481d6e44288c9e47)] - test: case for injecting incorrect script (#49) (hyj1991 <>) + +2.15.1 / 2021-09-15 +================== + +**features** + * [[`1a7f09c`](http://github.com/eggjs/egg-scripts/commit/1a7f09c707becaca42522ee415da0fe5961a6ad5)] - feat: support pkgInfo.eggScriptsConfig.require (#47) (hyj1991 <>) + +**others** + * [[`a68ac67`](http://github.com/eggjs/egg-scripts/commit/a68ac679b0eee4eff19c9e5d40ca80409ddf02eb)] - Revert "feat: support pkgInfo.egg.require (#45)" (#48) (hyj1991 <>) + +2.15.0 / 2021-09-13 +================== + +**features** + * [[`fe179fd`](http://github.com/eggjs/egg-scripts/commit/fe179fda909cd7eb5b6497357202185a4ecf5ec6)] - feat: support pkgInfo.egg.require (#45) (TZ | 天猪 <>) + +2.14.0 / 2021-06-11 +================== + +**features** + * [[`f0a342f`](http://github.com/eggjs/egg-scripts/commit/f0a342ffcd3ec1823eb2d42a9dd96c075cea3754)] - feat: --no-deprecation (#44) (TZ | 天猪 <>) + +2.13.0 / 2020-02-25 +================== + +**features** + * [[`c0ba739`](http://github.com/eggjs/egg-scripts/commit/c0ba73900642e488b0e6306ea028ef547ceedfae)] - feat: support stop timeout (#43) (hui <>) + +2.12.0 / 2019-12-16 +================== + +**features** + * [[`20483fd`](http://github.com/eggjs/egg-scripts/commit/20483fd56ce51238431fb095ede1c768a99470f2)] - feat: support eggScriptsConfig in package.json (#41) (Yiyu He <>) + +2.11.1 / 2019-10-10 +================== + +**fixes** + * [[`de61980`](http://github.com/eggjs/egg-scripts/commit/de61980f772c8a24010d3f078658f8c55b072067)] - fix: start command should exit after child process exit when no daemon mode (#39) (killa <>) + +**others** + * [[`7ae9cb0`](http://github.com/eggjs/egg-scripts/commit/7ae9cb054cb91ea7ac1e615e1e3a7fcdaba5f980)] - test: add egg@1 and egg@2 with travis (#36) (Maledong <>) + +2.11.0 / 2018-12-17 +=================== + + * feat(stop): only sleep when master process exists (#34) + * fix: stop process only if the title matches exactly (#35) + +2.10.0 / 2018-10-10 +================== + +**fixes** + * [[`4768950`](http://github.com/eggjs/egg-scripts/commit/4768950d29398031fd6ae129a981c60e308bff0a)] - fix: replace command by args in ps (#29) (Baffin Lee <>) + +**others** + * [[`f31efb9`](http://github.com/eggjs/egg-scripts/commit/f31efb9133c5edc6176371ca725198f1b43b9aab)] - feat: support customize node path (#32) (Yiyu He <>) + * [[`c2479dc`](http://github.com/eggjs/egg-scripts/commit/c2479dc6416386b654fc6e918a4dbd575cc0639e)] - chore: update version (TZ <>) + +2.9.1 / 2018-08-24 +================== + + * fix: replace command by args in ps (#29) + +2.9.0 / 2018-08-23 +================== + +**features** + * [[`1367883`](http://github.com/eggjs/egg-scripts/commit/1367883804e5ab1ece88831ea4d1a934ee757f81)] - feat: add ipc channel in nonDaemon mode (#28) (Khaidi Chu <>) + +**others** + * [[`262ef4c`](http://github.com/eggjs/egg-scripts/commit/262ef4c97179dbf6f8de2eb0547eef4cbc56bf92)] - chore: add license and issues link (#27) (Haoliang Gao <>) + +2.8.1 / 2018-08-19 +================== + +**fixes** + * [[`b98fd03`](http://github.com/eggjs/egg-scripts/commit/b98fd03d1e3aaed68004b881f0b3d42fe47341dd)] - fix: use execFile instead of exec for security reason (#26) (fengmk2 <>) + +2.8.0 / 2018-08-10 +================== + +**others** + * [[`dac29f7`](http://github.com/eggjs/egg-scripts/commit/dac29f73ed2dfc18edc2e8743ffd509af8ab0f4a)] - refactor: add `this.exit` to instead of `process.exit` (#25) (Khaidi Chu <>) + +2.7.0 / 2018-08-10 +================== + +**features** + * [[`22faa4c`](http://github.com/eggjs/egg-scripts/commit/22faa4cfbb84cc5bc819d981dce962d8f95f8357)] - feat: stop command support windows (#22) (Baffin Lee <>) + +**others** + * [[`e07726c`](http://github.com/eggjs/egg-scripts/commit/e07726c176a89dd63482b588868fd1feaab1fba6)] - refactor: raw spawn call to instead of helper.spawn in start non-daemon mode (#23) (Khaidi Chu <>) + +2.6.0 / 2018-04-03 +================== + + * feat: provides source map support for stack traces (#19) + +2.5.1 / 2018-02-06 +================== + + * chore: add description for ignore-stderr (#18) + +2.5.0 / 2017-12-12 +================== + +**features** + * [[`b5559d5`](http://github.com/eggjs/egg-scripts/commit/b5559d54228543b5422047e6f056829df11f8c87)] - feat: support --ignore-error (#17) (TZ | 天猪 <>) + +2.4.0 / 2017-11-30 +================== + +**features** + * [[`8eda3d1`](https://github.com/eggjs/egg-scripts/commit/8eda3d10cfea5757f220fd82b562fd5fef433440)] - feat: add `${baseDir}/.node/bin` to PATH if exists (#14) (fengmk2 <>) + +**others** + * [[`4dd24a4`](https://github.com/eggjs/egg-scripts/commit/4dd24a45d92b2c2a8e1e450e0f13ba4143550ca9)] - test: add testcase for #12 (#13) (Haoliang Gao <>) + +2.3.0 / 2017-11-29 +================== + +**features** + * [[`4c41319`](http://github.com/eggjs/egg-scripts/commit/4c41319f9e309402b2ccb5c7afd5a6d3cda2453f)] - feat: support stop --title (#16) (TZ | 天猪 <>) + +2.2.0 / 2017-11-22 +================== + +**features** + * [[`ac58d00`](http://github.com/eggjs/egg-scripts/commit/ac58d00a974fdfff6b5c722743e4b32174963c52)] - feat: cwd maybe not baseDir (#15) (zhennann <>) + +2.1.1 / 2017-11-14 +================== + +**fixes** + * [[`7324d99`](http://github.com/eggjs/egg-scripts/commit/7324d99b504cac5fef7dbf280f7d9e6243c16bb7)] - fix: should stop app when baseDir is symlink (#12) (Haoliang Gao <>) + +2.1.0 / 2017-10-16 +================== + +**features** + * [[`ac40135`](http://github.com/eggjs/egg-scripts/commit/ac40135d5b9a3200ea1bdfdb19d0f7e12d0c511a)] - feat: add eggctl bin (#10) (Haoliang Gao <>) + +2.0.0 / 2017-10-13 +================== + +**features** + * [[`0f7ca50`](http://github.com/eggjs/egg-scripts/commit/0f7ca502999c06a9cb05d8e5617f6045704511df)] - feat: [BREAKING_CHANGE] check the status of app when start on daemon (#9) (Haoliang Gao <>) + +**others** + * [[`cfd0d2f`](http://github.com/eggjs/egg-scripts/commit/cfd0d2f67845fffb9d5974514b65e43b22ed8040)] - refactor: modify the directory of logDir (#8) (Haoliang Gao <>) + +1.2.0 / 2017-09-11 +================== + +**features** + * [[`c0300b8`](http://github.com/eggjs/egg-scripts/commit/c0300b8c657fe4f75ca388061f6cb3de9864a743)] - feat: log success at daemon mode (#7) (TZ | 天猪 <>) + +**others** + * [[`fdd273c`](http://github.com/eggjs/egg-scripts/commit/fdd273c2d6f15d104288fef4d699627a7cf701d9)] - test: add cluster-config fixture (#4) (TZ | 天猪 <>) + +1.1.2 / 2017-09-01 +================== + + * fix: should not pass undefined env (#6) + * docs: fix stop typo (#5) + +1.1.1 / 2017-08-29 +================== + + * fix: should set title correct (#3) + +1.1.0 / 2017-08-16 +================== + + * feat: remove env default value (#2) + +1.0.0 / 2017-08-02 +================== + + * feat: first implementation (#1) diff --git a/History.md b/History.md deleted file mode 100644 index cbdc986..0000000 --- a/History.md +++ /dev/null @@ -1,30 +0,0 @@ - -1.2.0 / 2017-09-11 -================== - -**features** - * [[`c0300b8`](http://github.com/eggjs/egg-scripts/commit/c0300b8c657fe4f75ca388061f6cb3de9864a743)] - feat: log success at daemon mode (#7) (TZ | 天猪 <>) - -**others** - * [[`fdd273c`](http://github.com/eggjs/egg-scripts/commit/fdd273c2d6f15d104288fef4d699627a7cf701d9)] - test: add cluster-config fixture (#4) (TZ | 天猪 <>) - -1.1.2 / 2017-09-01 -================== - - * fix: should not pass undefined env (#6) - * docs: fix stop typo (#5) - -1.1.1 / 2017-08-29 -================== - - * fix: should set title correct (#3) - -1.1.0 / 2017-08-16 -================== - - * feat: remove env default value (#2) - -1.0.0 / 2017-08-02 -================== - - * feat: first implementation (#1) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7295685 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 04dabf7..0badde0 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,44 @@ -# egg-scripts +# @eggjs/scripts + +[![NPM version][npm-image]][npm-url] +[![Node.js CI](https://github.com/eggjs/scripts/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/scripts/actions/workflows/nodejs.yml) +[![Test coverage][codecov-image]][codecov-url] +[![npm download][download-image]][download-url] +[![Node.js Version](https://img.shields.io/node/v/@eggjs/scripts.svg?style=flat)](https://nodejs.org/en/download/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) + +[npm-image]: https://img.shields.io/npm/v/@eggjs/scripts.svg?style=flat-square +[npm-url]: https://npmjs.org/package/@eggjs/scripts +[codecov-image]: https://codecov.io/github/eggjs/scripts/coverage.svg?branch=master +[codecov-url]: https://codecov.io/github/eggjs/scripts?branch=master +[download-image]: https://img.shields.io/npm/dm/@eggjs/scripts.svg?style=flat-square +[download-url]: https://npmjs.org/package/@eggjs/scripts deploy tool for egg project. +**Note: Windows is partially supported, see [#22](https://github.com/eggjs/egg-scripts/pull/22)** + ## Install ```bash -$ npm i egg-scripts --save +npm i @eggjs/scripts --save ``` ## Usage -Add `egg-scripts` to `package.json` scripts: +Add `eggctl` to `package.json` scripts: ```json { "scripts": { - "start": "egg-scripts start --daemon", - "stop": "egg-scripts stop" + "start": "eggctl start --daemon", + "stop": "eggctl stop" } } ``` Then run as: + - `npm start` - `npm stop` @@ -34,33 +51,73 @@ Then run as: Start egg at prod mode. ```bash -$ egg-scripts start [options] [baseDir] +$ eggctl start [options] [baseDir] + # Usage -# egg-scripts start --port=7001 -# egg-scripts start ./server +# eggctl start --port=7001 +# eggctl start ./server ``` - **Arguments** - `baseDir` - directory of application, default to `process.cwd()`. - **Options** - `port` - listening port, default to `process.env.PORT`, if unset, egg will use `7001` as default. - - `title` - process title description, use for kill grep, default to `egg-server-APPNAME`. + - `title` - process title description, use for kill grep, default to `egg-server-${APP_NAME}`. - `workers` - numbers of app workers, default to `process.env.EGG_WORKERS`, if unset, egg will use `os.cpus().length` as default. - - `daemon` - whether run at background daemon mode. + - `daemon` - whether run at background daemon mode, don't use it if in docker mode. - `framework` - specify framework that can be absolute path or npm package, default to auto detect. - - `env` - egg server env, default to `process.env.EGG_SERVER_ENV`, recommended to keep empty then use framwork default env. + - `env` - server env, default to `process.env.EGG_SERVER_ENV`, recommended to keep empty then use framwork default env. + - `stdout` - customize stdout file, default to `$HOME/logs/master-stdout.log`. + - `stderr` - customize stderr file, default to `$HOME/logs/master-stderr.log`. + - `timeout` - the maximum timeout when app starts, default to 300s. + - `ignore-stderr` - whether ignore stderr when app starts. + - `sourcemap` / `typescript` / `ts` - provides source map support for stack traces. + - `node` - customize node command path, default will find node from $PATH ### stop Stop egg gracefull. -**Note:** **Windows is not supported yet**, try to kill master process which command contains `start-cluster` or `--title=egg-server` yourself, good luck. +**Note:** if exec without `--title`, it will kill all egg process. ```bash -# stop egg -$ egg-scripts stop [baseDir] -# egg-scripts stop ./server +$ eggctl stop [options] + +# Usage +# eggctl stop --title=example ``` -- **Arguments** - - `baseDir` - directory of application, default to `process.cwd()`. \ No newline at end of file +- **Options** + - `title` - process title description, use for kill grep. + - `timeout` - the maximum timeout when app stop, default to 5s. + +## Options in `package.json` + +In addition to the command line specification, options can also be specified in `package.json`. However, the command line designation takes precedence. + +```js +{ + "eggScriptsConfig": { + "port": 1234, + "ignore-stderr": true, + // will pass as `node --max-http-header-size=20000` + "node-options--max-http-header-size": "20000", + // will pass as `node --allow-wasi` + "node-options--allow-wasi": true + } +} +``` + +## Questions & Suggestions + +Please open an issue [here](https://github.com/eggjs/egg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc). + +## License + +[MIT](LICENSE) + +## Contributors + +[![Contributors](https://contrib.rocks/image?repo=eggjs/scripts)](https://github.com/eggjs/scripts/graphs/contributors) + +Made with [contributors-img](https://contrib.rocks). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 3d15e52..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,15 +0,0 @@ -environment: - matrix: - - nodejs_version: '6' - - nodejs_version: '8' - -install: - - ps: Install-Product node $env:nodejs_version - - npm i npminstall && node_modules\.bin\npminstall - -test_script: - - node --version - - npm --version - - npm run test - -build: off diff --git a/bin/dev.cmd b/bin/dev.cmd new file mode 100644 index 0000000..cec553b --- /dev/null +++ b/bin/dev.cmd @@ -0,0 +1,3 @@ +@echo off + +node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* diff --git a/bin/dev.js b/bin/dev.js new file mode 100755 index 0000000..f0b6195 --- /dev/null +++ b/bin/dev.js @@ -0,0 +1,5 @@ +#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning + +import { execute } from '@oclif/core'; + +await execute({ development: true, dir: import.meta.url }); diff --git a/bin/egg-scripts.js b/bin/egg-scripts.js deleted file mode 100755 index a77422c..0000000 --- a/bin/egg-scripts.js +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const Command = require('..'); - -new Command().start(); diff --git a/bin/run.cmd b/bin/run.cmd new file mode 100644 index 0000000..968fc30 --- /dev/null +++ b/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* diff --git a/bin/run.js b/bin/run.js new file mode 100755 index 0000000..176d2af --- /dev/null +++ b/bin/run.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { execute } from '@oclif/core'; + +await execute({ dir: import.meta.url }); diff --git a/index.js b/index.js deleted file mode 100644 index 5fd74e0..0000000 --- a/index.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -const path = require('path'); -const Command = require('./lib/command'); - -class EggScripts extends Command { - constructor(rawArgv) { - super(rawArgv); - this.usage = 'Usage: egg-scripts [command] [options]'; - - // load directory - this.load(path.join(__dirname, 'lib/cmd')); - } -} - -module.exports = exports = EggScripts; -exports.Command = Command; -exports.StartCommand = require('./lib/cmd/start'); -exports.StopCommand = require('./lib/cmd/stop'); diff --git a/lib/cmd/start.js b/lib/cmd/start.js deleted file mode 100644 index 783d304..0000000 --- a/lib/cmd/start.js +++ /dev/null @@ -1,166 +0,0 @@ -'use strict'; - -const path = require('path'); -const mkdirp = require('mz-modules/mkdirp'); -const homedir = require('node-homedir'); -const utils = require('egg-utils'); -const fs = require('mz/fs'); -const moment = require('moment'); -const spawn = require('child_process').spawn; -const Command = require('../command'); - -class StartCommand extends Command { - constructor(rawArgv) { - super(rawArgv); - this.usage = 'Usage: egg-scripts start [options] [baseDir]'; - this.serverBin = path.join(__dirname, '../start-cluster'); - - this.options = { - title: { - description: 'process title description, use for kill grep, default to `egg-server-APPNAME`', - type: 'string', - }, - workers: { - description: 'numbers of app workers, default to `os.cpus().length`', - type: 'number', - alias: [ 'c', 'cluster' ], - default: process.env.EGG_WORKERS, - }, - port: { - description: 'listening port, default to `process.env.PORT`', - type: 'number', - alias: 'p', - default: process.env.PORT, - }, - env: { - description: 'egg server env, default to `process.env.EGG_SERVER_ENV`', - default: process.env.EGG_SERVER_ENV, - }, - framework: { - description: 'specify framework that can be absolute path or npm package', - type: 'string', - }, - daemon: { - description: 'whether run at background daemon mode', - type: 'boolean', - }, - }; - } - - get description() { - return 'Start server at prod mode'; - } - - * run(context) { - const argv = Object.assign({}, context.argv); - - // egg-script start - // egg-script start ./server - // egg-script start /opt/app - let baseDir = argv._[0] || context.cwd; - if (!path.isAbsolute(baseDir)) baseDir = path.join(context.cwd, baseDir); - argv.baseDir = baseDir; - - const isDaemon = argv.daemon; - - argv.framework = utils.getFrameworkPath({ - framework: argv.framework, - baseDir, - }); - - const env = context.env; - env.PWD = baseDir; - env.HOME = homedir(); - env.NODE_ENV = 'production'; - - // cli argv -> process.env.EGG_SERVER_ENV -> `undefined` then egg will use `prod` - if (argv.env) { - // if undefined, should not pass key due to `spwan`, https://github.com/nodejs/node/blob/master/lib/child_process.js#L470 - env.EGG_SERVER_ENV = argv.env; - argv.env = undefined; - } - - const pkgInfo = require(path.join(baseDir, 'package.json')); - const logDir = path.join(env.HOME, 'logs', pkgInfo.name); - - argv.title = argv.title || `egg-server-${pkgInfo.name}`; - - // adjust env for win - let envPath = env.PATH || env.Path; - if (envPath) { - // for nodeinstall - envPath = path.join(baseDir, 'node_modules/.bin') + path.delimiter + envPath; - } - - // for alinode - env.ENABLE_NODE_LOG = 'YES'; - env.NODE_LOG_DIR = env.NODE_LOG_DIR || path.join(logDir, 'alinode'); - yield mkdirp(env.NODE_LOG_DIR); - - // remove unused properties, alias had been remove by `removeAlias` - argv._ = undefined; - argv.$0 = undefined; - argv.daemon = undefined; - - const options = { - execArgv: context.execArgv, - env, - stdio: 'inherit', - detached: false, - }; - - this.logger.info(`starting egg application at ${baseDir}`); - - const eggArgs = [ this.serverBin, JSON.stringify(argv), `--title=${argv.title}` ]; - this.logger.info('run node %s', eggArgs.join(' ')); - - // whether run in the background. - if (isDaemon) { - this.logger.info(`save log file to ${logDir}`); - const { stdout, stderr } = yield getRotatelog(logDir); - options.stdio = [ 'ignore', stdout, stderr, 'ipc' ]; - options.detached = true; - - const child = spawn('node', eggArgs, options); - child.on('message', msg => { - if (msg && msg.action === 'egg-ready') { - this.logger.info(`egg started on ${msg.data.address}`); - child.unref(); - child.disconnect(); - process.exit(0); - } - }); - } else { - // signal event had been handler at common-bin helper - this.helper.spawn('node', eggArgs, options); - } - } -} - -function* getRotatelog(logDir) { - const stdoutPath = path.join(logDir, 'master-stdout.log'); - const stderrPath = path.join(logDir, 'master-stderr.log'); - - // format style: .20150602.193100 - const timestamp = moment().format('.YYYYMMDD.HHmmss'); - - yield mkdirp(logDir); - - /* istanbul ignore else */ - if (yield fs.exists(stdoutPath)) { - // Note: rename last log to next start time, not when last log file created - yield fs.rename(stdoutPath, stdoutPath + timestamp); - } - - /* istanbul ignore else */ - if (yield fs.exists(stderrPath)) { - yield fs.rename(stderrPath, stderrPath + timestamp); - } - - return yield { - stdout: fs.open(stdoutPath, 'a'), - stderr: fs.open(stderrPath, 'a'), - }; -} - -module.exports = StartCommand; diff --git a/lib/cmd/stop.js b/lib/cmd/stop.js deleted file mode 100644 index 250014e..0000000 --- a/lib/cmd/stop.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -const path = require('path'); -const sleep = require('mz-modules/sleep'); -const Command = require('../command'); - -class StopCommand extends Command { - - constructor(rawArgv) { - super(rawArgv); - this.usage = 'Usage: egg-scripts stop [baseDir]'; - this.serverBin = path.join(__dirname, '../start-cluster'); - } - - get description() { - return 'Stop server'; - } - - * run(context) { - /* istanbul ignore next */ - if (process.platform === 'win32') { - this.logger.warn('Windows is not supported, try to kill master process which command contains `start-cluster` or `--type=egg-server` yourself, good luck.'); - process.exit(0); - } - - const { argv } = context; - - // egg-script stop - // egg-script stop ./server - // egg-script stop /opt/app - let baseDir = argv._[0] || context.cwd; - if (!path.isAbsolute(baseDir)) baseDir = path.join(context.cwd, baseDir); - argv.baseDir = baseDir; - - this.logger.info(`stopping egg application at ${baseDir}`); - - // node /Users/tz/Workspaces/eggjs/egg-scripts/lib/start-cluster {"title":"egg-server","workers":4,"port":7001,"baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg"} - let processList = yield this.helper.findNodeProcess(item => { - const cmd = item.cmd; - return cmd.includes(this.serverBin) && cmd.includes(`"baseDir":"${baseDir}"`); - }); - let pids = processList.map(x => x.pid); - - if (pids.length) { - this.logger.info('got master pid %j', pids); - this.helper.kill(pids); - } else { - this.logger.warn('can\'t detect any running egg process'); - } - - // wait for 5s to confirm whether any worker process did not kill by master - yield sleep('5s'); - - // node --debug-port=5856 /Users/tz/Workspaces/eggjs/test/showcase/node_modules/_egg-cluster@1.8.0@egg-cluster/lib/agent_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406} - // node /Users/tz/Workspaces/eggjs/test/showcase/node_modules/_egg-cluster@1.8.0@egg-cluster/lib/app_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406} - processList = yield this.helper.findNodeProcess(item => { - const cmd = item.cmd; - return cmd.includes(`"baseDir":"${baseDir}"`) && (cmd.includes('app_worker.js') || cmd.includes('agent_worker.js')); - }); - pids = processList.map(x => x.pid); - - if (pids.length) { - this.logger.info('got worker/agent pids %j that is not killed by master', pids); - this.helper.kill(pids, 'SIGKILL'); - } - - this.logger.info('stopped'); - } -} - -module.exports = StopCommand; diff --git a/lib/command.js b/lib/command.js deleted file mode 100644 index 77f1a8e..0000000 --- a/lib/command.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const BaseCommand = require('common-bin'); -const Logger = require('zlogger'); -const helper = require('./helper'); - -class Command extends BaseCommand { - constructor(rawArgv) { - super(rawArgv); - - Object.assign(this.helper, helper); - - this.parserOptions = { - removeAlias: true, - removeCamelCase: true, - execArgv: true, - }; - - this.logger = new Logger({ - prefix: '[egg-scripts] ', - time: false, - }); - } -} - -module.exports = Command; diff --git a/lib/helper.js b/lib/helper.js deleted file mode 100644 index 881d869..0000000 --- a/lib/helper.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const runScript = require('runscript'); -const REGEX = /^\s*(\d+)\s+(.*)/; - -exports.findNodeProcess = function* (filterFn) { - const command = 'ps -eo "pid,command"'; - const stdio = yield runScript(command, { stdio: 'pipe' }); - const processList = stdio.stdout.toString().split('\n') - .reduce((arr, line) => { - if (!!line && !line.includes('/bin/sh') && line.includes('node')) { - const m = line.match(REGEX); - /* istanbul ignore else */ - if (m) { - const item = { pid: m[1], cmd: m[2] }; - if (!filterFn || filterFn(item)) { - arr.push(item); - } - } - } - return arr; - }, []); - return processList; -}; - -exports.kill = function(pids, signal) { - pids.forEach(pid => { - try { - process.kill(pid, signal); - } catch (err) { /* istanbul ignore next */ - if (err.code !== 'ESRCH') { - throw err; - } - } - }); -}; diff --git a/lib/start-cluster b/lib/start-cluster deleted file mode 100644 index ebb1efe..0000000 --- a/lib/start-cluster +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -const options = JSON.parse(process.argv[2]); -require(options.framework).startCluster(options); diff --git a/package.json b/package.json index 902f861..e197530 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,103 @@ { - "name": "egg-scripts", - "version": "1.2.0", - "description": "deploy tool for egg project", - "main": "index.js", - "bin": { - "egg-scripts": "bin/egg-scripts.js" + "name": "@eggjs/scripts", + "version": "4.0.0", + "publishConfig": { + "access": "public" }, + "description": "deploy tool for egg project", "dependencies": { - "common-bin": "^2.5.0", - "egg-utils": "^2.2.0", - "moment": "^2.18.1", - "mz": "^2.6.0", - "mz-modules": "^1.0.0", - "node-homedir": "^1.0.0", - "runscript": "^1.3.0", - "zlogger": "^1.1.0" + "@eggjs/utils": "^4.2.1", + "@oclif/core": "^4.2.0", + "common-bin": "^3.0.1", + "mz": "^2.7.0", + "mz-modules": "^2.1.0", + "node-homedir": "^2.0.0", + "runscript": "^2.0.1", + "source-map-support": "^0.5.21", + "utility": "^2.4.0" }, "devDependencies": { - "autod": "^2.9.0", - "coffee": "^4.1.0", - "egg": "^1.7.0", - "egg-bin": "^4.1.0", - "egg-ci": "^1.8.0", - "eslint": "^4.4.1", - "eslint-config-egg": "^5.0.0", - "mm": "^2.1.0", - "urllib": "^2.24.0", - "webstorm-disable-index": "^1.2.0" + "@arethetypeswrong/cli": "^0.17.1", + "@eggjs/bin": "^7.0.1", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "coffee": "^5.5.1", + "egg": "beta", + "eslint": "8", + "eslint-config-egg": "14", + "mm": "^4.0.1", + "rimraf": "6", + "ts-node": "^10.9.2", + "tshy": "3", + "tshy-after": "1", + "typescript": "5", + "urllib": "4" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.19.0" }, "scripts": { - "test": "npm run lint -- --fix && npm run test-local", - "test-local": "egg-bin test", - "cov": "egg-bin cov", - "lint": "eslint .", - "ci": "egg-bin pkgfiles --check && npm run lint && npm run cov", - "autod": "autod" - }, - "files": [ - "index.js", - "lib", - "bin" - ], - "ci": { - "version": "6, 8" + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix && npm run prepublishOnly", + "test": "egg-bin test", + "posttest": "npm run clean", + "preci": "npm run clean && npm run lint && npm run prepublishOnly", + "ci": "egg-bin test", + "postci": "npm run clean", + "clean": "rimraf dist", + "prepublishOnly": "tshy && tshy-after && attw --pack" }, "bug": { "url": "https://github.com/eggjs/egg/issues" }, - "homepage": "https://github.com/eggjs/egg-scripts", + "homepage": "https://github.com/eggjs/scripts", "repository": { "type": "git", - "url": "git@github.com:eggjs/egg-scripts.git" + "url": "git@github.com:eggjs/scripts.git" }, "author": "TZ ", - "license": "MIT" -} \ No newline at end of file + "license": "MIT", + "oclif": { + "bin": "eggctl", + "commands": "./dist/esm/commands", + "dirname": "eggctl", + "topicSeparator": " ", + "additionalHelpFlags": [ + "-h" + ] + }, + "bin": { + "egg-scripts": "./bin/run.js", + "eggctl": "./bin/run.js" + }, + "type": "module", + "tshy": { + "exports": { + ".": "./src/index.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" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "bin", + "dist", + "src", + "scripts" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" +} diff --git a/scripts/start-cluster.cjs b/scripts/start-cluster.cjs new file mode 100755 index 0000000..a399f75 --- /dev/null +++ b/scripts/start-cluster.cjs @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { debuglog } = require('node:util'); +const { importModule } = require('@eggjs/utils'); + +const debug = debuglog('@eggjs/scripts/scripts/start-cluster'); + +async function main() { + debug('argv: %o', process.argv); + const options = JSON.parse(process.argv[2]); + debug('start cluster options: %o', options); + const { startCluster } = await importModule(options.framework); + await startCluster(options); +} + +main(); diff --git a/scripts/start-cluster.mjs b/scripts/start-cluster.mjs new file mode 100755 index 0000000..5792705 --- /dev/null +++ b/scripts/start-cluster.mjs @@ -0,0 +1,14 @@ +import { debuglog } from 'node:util'; +import { importModule } from '@eggjs/utils'; + +const debug = debuglog('@eggjs/scripts/scripts/start-cluster'); + +async function main() { + debug('argv: %o', process.argv); + const options = JSON.parse(process.argv[2]); + debug('start cluster options: %o', options); + const { startCluster } = await importModule(options.framework); + await startCluster(options); +} + +main(); diff --git a/src/baseCommand.ts b/src/baseCommand.ts new file mode 100644 index 0000000..ec76132 --- /dev/null +++ b/src/baseCommand.ts @@ -0,0 +1,68 @@ +import { debuglog } from 'node:util'; +import { Command, Flags, Interfaces } from '@oclif/core'; +import { PackageEgg } from './types.js'; +import { readJSON } from 'utility'; +import path from 'node:path'; + +const debug = debuglog('@eggjs/scripts/baseCommand'); + +type Flags = Interfaces.InferredFlags; +type Args = Interfaces.InferredArgs; + +export abstract class BaseCommand extends Command { + // add the --json flag + static enableJsonFlag = false; + + // define flags that can be inherited by any command that extends BaseCommand + static baseFlags = { + // 'log-level': Flags.option({ + // default: 'info', + // helpGroup: 'GLOBAL', + // options: ['debug', 'warn', 'error', 'info', 'trace'] as const, + // summary: 'Specify level for logging.', + // })(), + }; + + protected flags!: Flags; + protected args!: Args; + + protected env = { ...process.env }; + protected pkg: Record; + protected isESM: boolean; + protected pkgEgg: PackageEgg; + protected globalExecArgv: string[] = []; + + public async init(): Promise { + await super.init(); + debug('[init] raw args: %o, NODE_ENV: %o', this.argv, this.env.NODE_ENV); + const { args, flags } = await this.parse({ + flags: this.ctor.flags, + baseFlags: (super.ctor as typeof BaseCommand).baseFlags, + enableJsonFlag: this.ctor.enableJsonFlag, + args: this.ctor.args, + strict: this.ctor.strict, + }); + this.flags = flags as Flags; + this.args = args as Args; + } + + protected async initBaseInfo(baseDir: string) { + const pkg = await readJSON(path.join(baseDir, 'package.json')); + this.pkg = pkg; + this.pkgEgg = pkg.egg ?? {}; + this.isESM = pkg.type === 'module'; + debug('[initBaseInfo] baseDir: %o, pkgEgg: %o, isESM: %o', baseDir, this.pkgEgg, this.isESM); + } + + protected async catch(err: Error & {exitCode?: number}): Promise { + // add any custom logic to handle errors from the command + // or simply return the parent class error handling + return super.catch(err); + } + + protected async finally(_: Error | undefined): Promise { + // called after run and catch regardless of whether or not the command errored + return super.finally(_); + } +} + diff --git a/src/commands/start.ts b/src/commands/start.ts new file mode 100644 index 0000000..2b62d1c --- /dev/null +++ b/src/commands/start.ts @@ -0,0 +1,384 @@ +import { debuglog, promisify } from 'node:util'; +import path from 'node:path'; +import { scheduler } from 'node:timers/promises'; +import { spawn, SpawnOptions, ChildProcess, execFile as _execFile } from 'node:child_process'; +import { mkdir, rename, stat, open } from 'node:fs/promises'; +import { homedir } from 'node-homedir'; +import { Args, Flags } from '@oclif/core'; +import { getFrameworkPath, importResolve } from '@eggjs/utils'; +import { readJSON, exists, getDateStringParts } from 'utility'; +import { BaseCommand } from '../baseCommand.js'; +import { getSourceDirname } from '../helper.js'; + +const debug = debuglog('@eggjs/scripts/commands/start'); + +const execFile = promisify(_execFile); + +export interface FrameworkOptions { + baseDir: string; + framework?: string; +} + +export default class Start extends BaseCommand { + static override description = 'Start server at prod mode'; + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + ]; + + static override args = { + baseDir: Args.string({ + description: 'directory of application', + required: false, + }), + }; + + static override flags = { + title: Flags.string({ + description: 'process title description, use for kill grep, default to `egg-server-${APP_NAME}`', + }), + framework: Flags.string({ + description: 'specify framework that can be absolute path or npm package', + }), + port: Flags.integer({ + description: 'listening port, default to `process.env.PORT`', + char: 'p', + }), + workers: Flags.integer({ + char: 'c', + aliases: [ 'cluster' ], + description: 'numbers of app workers, default to `process.env.EGG_WORKERS` or `os.cpus().length`', + }), + env: Flags.string({ + description: 'server env, default to `process.env.EGG_SERVER_ENV`', + default: process.env.EGG_SERVER_ENV, + }), + daemon: Flags.boolean({ + description: 'whether run at background daemon mode', + }), + stdout: Flags.string({ + description: 'customize stdout file', + }), + stderr: Flags.string({ + description: 'customize stderr file', + }), + timeout: Flags.integer({ + description: 'the maximum timeout(ms) when app starts', + default: 300 * 1000, + }), + 'ignore-stderr': Flags.boolean({ + description: 'whether ignore stderr when app starts', + }), + node: Flags.string({ + description: 'customize node command path', + default: 'node', + }), + require: Flags.string({ + summary: 'require the given module', + char: 'r', + multiple: true, + }), + sourcemap: Flags.boolean({ + summary: 'whether enable sourcemap support, will load `source-map-support` etc', + aliases: [ 'ts', 'typescript' ], + }), + }; + + isReady = false; + #child: ChildProcess; + + protected async getFrameworkPath(options: FrameworkOptions) { + return getFrameworkPath(options); + } + + protected async getFrameworkName(frameworkPath: string) { + const pkgPath = path.join(frameworkPath, 'package.json'); + let name = 'egg'; + try { + const pkg = await readJSON(pkgPath); + if (pkg.name) { + name = pkg.name; + } + } catch { + // ignore + } + return name; + } + + protected async getServerBin() { + const serverBinName = this.isESM ? 'start-cluster.mjs' : 'start-cluster.cjs'; + // for src paths, `./src/commands/start.js` + let serverBin = path.join(getSourceDirname(), '../scripts', serverBinName); + if (!(await exists(serverBin))) { + // for dist paths, `./dist/esm/commands/start.js` + serverBin = path.join(getSourceDirname(), '../../scripts', serverBinName); + } + return serverBin; + } + + public async run(): Promise { + const { args, flags } = this; + // context.execArgvObj = context.execArgvObj || {}; + // const { argv, env, cwd, execArgvObj } = context; + const HOME = homedir(); + const logDir = path.join(HOME, 'logs'); + + // eggctl start + // eggctl start ./server + // eggctl start /opt/app + const cwd = process.cwd(); + let baseDir = args.baseDir || cwd; + if (!path.isAbsolute(baseDir)) { + baseDir = path.join(cwd, baseDir); + } + await this.initBaseInfo(baseDir); + + flags.framework = await this.getFrameworkPath({ + framework: flags.framework, + baseDir, + }); + + const frameworkName = await this.getFrameworkName(flags.framework); + + flags.title = flags.title || `egg-server-${this.pkg.name}`; + + flags.stdout = flags.stdout || path.join(logDir, 'master-stdout.log'); + flags.stderr = flags.stderr || path.join(logDir, 'master-stderr.log'); + + if (flags.workers === undefined && process.env.EGG_WORKERS) { + flags.workers = Number(process.env.EGG_WORKERS); + } + + // normalize env + this.env.HOME = HOME; + this.env.NODE_ENV = 'production'; + + // it makes env big but more robust + this.env.PATH = this.env.Path = [ + // for nodeinstall + path.join(baseDir, 'node_modules/.bin'), + // support `.node/bin`, due to npm5 will remove `node_modules/.bin` + path.join(baseDir, '.node/bin'), + // adjust env for win + this.env.PATH || this.env.Path, + ].filter(x => !!x).join(path.delimiter); + + // for alinode + this.env.ENABLE_NODE_LOG = 'YES'; + this.env.NODE_LOG_DIR = this.env.NODE_LOG_DIR || path.join(logDir, 'alinode'); + await mkdir(this.env.NODE_LOG_DIR, { recursive: true }); + + // cli argv -> process.env.EGG_SERVER_ENV -> `undefined` then egg will use `prod` + if (flags.env) { + // if undefined, should not pass key due to `spawn`, https://github.com/nodejs/node/blob/master/lib/child_process.js#L470 + this.env.EGG_SERVER_ENV = flags.env; + } + + // additional execArgv + const execArgv: string[] = [ + '--no-deprecation', + '--trace-warnings', + ]; + if (this.pkgEgg.revert) { + const reverts = Array.isArray(this.pkgEgg.revert) ? this.pkgEgg.revert : [ this.pkgEgg.revert ]; + for (const revert of reverts) { + execArgv.push(`--security-revert=${revert}`); + } + } + + // pkg.eggScriptsConfig.require + const scriptsConfig: Record = this.pkg.eggScriptsConfig; + if (scriptsConfig?.require) { + scriptsConfig.require = Array.isArray(scriptsConfig.require) ? scriptsConfig.require : [ scriptsConfig.require ]; + flags.require = [ ...scriptsConfig.require, ...(flags.require ?? []) ]; + } + + // read argv from eggScriptsConfig in package.json + if (scriptsConfig) { + for (const key in scriptsConfig) { + const v = scriptsConfig[key]; + if (key.startsWith('node-options--')) { + const newKey = key.replace('node-options--', ''); + if (v === true) { + // "node-options--allow-wasi": true + // => --allow-wasi + execArgv.push(`--${newKey}`); + } else { + // "node-options--max-http-header-size": "20000" + // => --max-http-header-size=20000 + execArgv.push(`--${newKey}=${v}`); + } + continue; + } + const existsValue = Reflect.get(flags, key); + if (existsValue === undefined) { + // only set if key is not pass from command line + Reflect.set(flags, key, v); + } + } + } + + // read `egg.typescript` from package.json + if (this.pkgEgg.typescript && flags.sourcemap === undefined) { + flags.sourcemap = true; + } + if (flags.sourcemap) { + const sourceMapSupport = importResolve('source-map-support/register.js', { + paths: [ getSourceDirname() ], + }); + if (this.isESM) { + execArgv.push('--import', sourceMapSupport); + } else { + execArgv.push('--require', sourceMapSupport); + } + } + + if (flags.port === undefined && process.env.PORT) { + flags.port = parseInt(process.env.PORT); + } + + debug('flags: %o, framework: %o, baseDir: %o, execArgv: %o', + flags, frameworkName, baseDir, execArgv); + + const command = flags.node; + const options: SpawnOptions = { + env: this.env, + stdio: 'inherit', + detached: false, + cwd: baseDir, + }; + + this.log('Starting %s application at %s', frameworkName, baseDir); + + // remove unused properties from stringify, alias had been remove by `removeAlias` + const ignoreKeys = [ 'env', 'daemon', 'stdout', 'stderr', 'timeout', 'ignore-stderr', 'node' ]; + const clusterOptions = stringify({ + ...flags, + baseDir, + }, ignoreKeys); + // Note: `spawn` is not like `fork`, had to pass `execArgv` yourself + const serverBin = await this.getServerBin(); + const eggArgs = [ ...execArgv, serverBin, clusterOptions, `--title=${flags.title}` ]; + const spawnScript = `${command} ${eggArgs.map(a => `'${a}'`).join(' ')}`; + this.log('Spawn %o', spawnScript); + + // whether run in the background. + if (flags.daemon) { + this.log(`Save log file to ${logDir}`); + const [ stdout, stderr ] = await Promise.all([ + getRotateLog(flags.stdout), + getRotateLog(flags.stderr), + ]); + options.stdio = [ 'ignore', stdout, stderr, 'ipc' ]; + options.detached = true; + const child = this.#child = spawn(command, eggArgs, options); + this.isReady = false; + child.on('message', (msg: any) => { + // https://github.com/eggjs/cluster/blob/master/src/master.ts#L119 + if (msg && msg.action === 'egg-ready') { + this.isReady = true; + this.log('%s started on %s', frameworkName, msg.data.address); + child.unref(); + child.disconnect(); + } + }); + + // check start status + await this.checkStatus(); + } else { + options.stdio = [ 'inherit', 'inherit', 'inherit', 'ipc' ]; + const child = this.#child = spawn(command, eggArgs, options); + child.once('exit', code => { + if (!code) return; + // command should exit after child process exit + this.exit(code); + }); + + // attach master signal to child + let signal; + const signals = [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ] as NodeJS.Signals[]; + signals.forEach(event => { + process.once(event, () => { + debug('Kill child %s with %s', child.pid, signal); + child.kill(event); + }); + }); + } + } + + protected async checkStatus() { + let count = 0; + let hasError = false; + let isSuccess = true; + const timeout = this.flags.timeout / 1000; + const stderrFile = this.flags.stderr!; + while (!this.isReady) { + try { + const stats = await stat(stderrFile); + if (stats && stats.size > 0) { + hasError = true; + break; + } + } catch (_) { + // nothing + } + + if (count >= timeout) { + this.logToStderr('Start failed, %ds timeout', timeout); + isSuccess = false; + break; + } + + await scheduler.wait(1000); + this.log('Wait Start: %d...', ++count); + } + + if (hasError) { + try { + const args = [ '-n', '100', stderrFile ]; + this.logToStderr('tail %s', args.join(' ')); + const { stdout: headStdout } = await execFile('head', args); + const { stdout: tailStdout } = await execFile('tail', args); + this.logToStderr('Got error when startup: '); + this.logToStderr(headStdout); + this.logToStderr('...'); + this.logToStderr(tailStdout); + } catch (err) { + this.logToStderr('ignore tail error: %s', err); + } + isSuccess = this.flags['ignore-stderr']; + this.logToStderr('Start got error, see %o', stderrFile); + this.logToStderr('Or use `--ignore-stderr` to ignore stderr at startup.'); + } + + if (!isSuccess) { + this.#child.kill('SIGTERM'); + await scheduler.wait(1000); + this.exit(1); + } + } +} + +function stringify(obj: Record, ignore: string[]) { + const result: Record = {}; + Object.keys(obj).forEach(key => { + if (!ignore.includes(key)) { + result[key] = obj[key]; + } + }); + return JSON.stringify(result); +} + +async function getRotateLog(logFile: string) { + await mkdir(path.dirname(logFile), { recursive: true }); + + if (await exists(logFile)) { + // format style: .20150602.193100 + const [ YYYY, MM, DD, HH, mm, ss ] = getDateStringParts(); + const timestamp = `.${YYYY}${MM}${DD}.${HH}${mm}${ss}`; + // Note: rename last log to next start time, not when last log file created + await rename(logFile, logFile + timestamp); + } + + return (await open(logFile, 'a')).fd; +} diff --git a/src/commands/stop.ts b/src/commands/stop.ts new file mode 100644 index 0000000..248ae1d --- /dev/null +++ b/src/commands/stop.ts @@ -0,0 +1,105 @@ +import { debuglog, format } from 'node:util'; +import { scheduler } from 'node:timers/promises'; +import { Args, Flags } from '@oclif/core'; +import { BaseCommand } from '../baseCommand.js'; +import { isWindows, findNodeProcess, NodeProcess, kill } from '../helper.js'; + +const debug = debuglog('@eggjs/scripts/commands/stop'); + +const osRelated = { + titleTemplate: isWindows ? '\\"title\\":\\"%s\\"' : '"title":"%s"', + // node_modules/@eggjs/cluster/dist/commonjs/app_worker.js + // node_modules/@eggjs/cluster/dist/esm/app_worker.js + appWorkerPath: /@eggjs[\/\\]cluster[\/\\]dist[\/\\](commonjs|esm)[\/\\]app_worker\.js/i, + // node_modules/@eggjs/cluster/dist/commonjs/agent_worker.js + // node_modules/@eggjs/cluster/dist/esm/agent_worker.js + agentWorkerPath: /@eggjs[\/\\]cluster[\/\\]dist[\/\\](commonjs|esm)[\/\\]agent_worker\.js/i, +}; + +export default class Stop extends BaseCommand { + static override description = 'Stop server'; + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + ]; + + static override args = { + baseDir: Args.string({ + description: 'directory of application', + required: false, + }), + }; + + static override flags = { + title: Flags.string({ + description: 'process title description, use for kill grep', + }), + timeout: Flags.integer({ + description: 'the maximum timeout(ms) when app stop', + default: 5000, + }), + }; + + public async run(): Promise { + const { flags } = this; + + this.log(`stopping egg application${flags.title ? ` with --title=${flags.title}` : ''}`); + + // node ~/eggjs/scripts/scripts/start-cluster.cjs {"title":"egg-server","workers":4,"port":7001,"baseDir":"~/eggjs/test/showcase","framework":"~/eggjs/test/showcase/node_modules/egg"} + let processList = await this.findNodeProcesses(item => { + const cmd = item.cmd; + const matched = flags.title ? + cmd.includes('start-cluster') && cmd.includes(format(osRelated.titleTemplate, flags.title)) : + cmd.includes('start-cluster'); + if (matched) { + debug('find master process: %o', item); + } + return matched; + }); + let pids = processList.map(x => x.pid); + + if (pids.length) { + this.log('got master pid %j, list:', pids); + this.log(''); + for (const item of processList) { + this.log('- %s: %o', item.pid, item.cmd); + } + this.log(''); + this.killProcesses(pids); + // wait for 5s to confirm whether any worker process did not kill by master + await scheduler.wait(flags.timeout); + } else { + this.logToStderr('can\'t detect any running egg process'); + } + + // node --debug-port=5856 /Users/tz/Workspaces/eggjs/test/showcase/node_modules/_egg-cluster@1.8.0@egg-cluster/lib/agent_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406} + // node /Users/tz/Workspaces/eggjs/test/showcase/node_modules/_egg-cluster@1.8.0@egg-cluster/lib/app_worker.js {"framework":"/Users/tz/Workspaces/eggjs/test/showcase/node_modules/egg","baseDir":"/Users/tz/Workspaces/eggjs/test/showcase","port":7001,"workers":2,"plugins":null,"https":false,"key":"","cert":"","title":"egg-server","clusterPort":52406} + // ~/bin/node --no-deprecation --trace-warnings ~/eggjs/examples/helloworld/node_modules/@eggjs/cluster/dist/commonjs/agent_worker.js {"baseDir":"~/eggjs/examples/helloworld","startMode":"process","framework":"~/eggjs/examples/helloworld/node_modules/egg","title":"egg-server-helloworld","workers":10,"clusterPort":58977} + processList = await this.findNodeProcesses(item => { + const cmd = item.cmd; + const matched = flags.title ? + (osRelated.appWorkerPath.test(cmd) || osRelated.agentWorkerPath.test(cmd)) && cmd.includes(format(osRelated.titleTemplate, flags.title)) : + (osRelated.appWorkerPath.test(cmd) || osRelated.agentWorkerPath.test(cmd)); + if (matched) { + debug('find app/agent worker process: %o', item); + } + return matched; + }); + pids = processList.map(x => x.pid); + + if (pids.length) { + this.log('got worker/agent pids %j that is not killed by master', pids); + this.killProcesses(pids); + } + + this.log('stopped'); + } + + protected async findNodeProcesses(filter: (item: NodeProcess) => boolean): Promise { + return findNodeProcess(filter); + } + + protected killProcesses(pids: number[], signal: NodeJS.Signals = 'SIGTERM') { + kill(pids, signal); + } +} diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..7c57cdb --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,62 @@ +import { runScript } from 'runscript'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const isWindows = process.platform === 'win32'; + +const REGEX = isWindows ? /^(.*)\s+(\d+)\s*$/ : /^\s*(\d+)\s+(.*)/; + +export interface NodeProcess { + pid: number; + cmd: string; +} + +export type FilterFunction = (item: NodeProcess) => boolean; + +export async function findNodeProcess(filterFn?: FilterFunction): Promise { + const command = isWindows ? + 'wmic Path win32_process Where "Name = \'node.exe\'" Get CommandLine,ProcessId' : + // command, cmd are alias of args, not POSIX standard, so we use args + 'ps -wweo "pid,args"'; + const stdio = await runScript(command, { stdio: 'pipe' }); + const processList = stdio.stdout!.toString().split('\n') + .reduce((arr, line) => { + if (!!line && !line.includes('/bin/sh') && line.includes('node')) { + const m = line.match(REGEX); + if (m) { + const item: NodeProcess = isWindows ? { pid: parseInt(m[2]), cmd: m[1] } : { pid: parseInt(m[1]), cmd: m[2] }; + if (filterFn?.(item)) { + arr.push(item); + } + } + } + return arr; + }, []); + return processList; +} + +export function kill(pids: number[], signal?: string | number) { + pids.forEach(pid => { + try { + process.kill(pid, signal); + } catch (err: any) { + if (err.code !== 'ESRCH') { + throw err; + } + } + }); +} + +export function getSourceDirname() { + if (typeof __dirname === 'string') { + return __dirname; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const __filename = fileURLToPath(import.meta.url); + return path.dirname(__filename); +} + +export function getSourceFilename(filename: string) { + return path.join(getSourceDirname(), filename); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6d1c425 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +import Start from './commands/start.js'; + +// exports.StopCommand = require('./lib/cmd/stop'); + +export * from './baseCommand.js'; +export { + Start, Start as StartCommand, +}; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b785f83 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export interface PackageEgg { + framework?: boolean; + typescript?: boolean; + tscompiler?: string; + declarations?: boolean; + revert?: string | string[]; + require?: string | string[]; + import?: string | string[]; +} diff --git a/test/egg-scripts.test.js b/test/egg-scripts.test.js deleted file mode 100644 index f48b991..0000000 --- a/test/egg-scripts.test.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -const coffee = require('coffee'); - -describe('test/egg-scripts.test.js', () => { - let app; - const eggBin = require.resolve('../bin/egg-scripts.js'); - - it('show help', done => { - app = coffee.fork(eggBin, [ '--help' ]); - app - // .debug() - .expect(/Usage: egg-scripts/) - .expect('code', 0) - .end(done); - }); -}); diff --git a/test/egg-scripts.test.ts b/test/egg-scripts.test.ts new file mode 100644 index 0000000..21e2323 --- /dev/null +++ b/test/egg-scripts.test.ts @@ -0,0 +1,26 @@ +import coffee from 'coffee'; +import { getSourceFilename } from '../src/helper.js'; + +describe('test/egg-scripts.test.ts', () => { + const eggBin = getSourceFilename('../bin/run.js'); + + it('show help', async () => { + await coffee.fork(eggBin, [ '--help' ]) + .debug() + .expect('stdout', /\$ eggctl \[COMMAND]/) + .expect('code', 0) + .end(); + + await coffee.fork(eggBin, [ 'start', '-h' ]) + .debug() + .expect('stdout', /\$ eggctl start \[BASEDIR] /) + .expect('code', 0) + .end(); + + await coffee.fork(eggBin, [ 'stop', '-h' ]) + .debug() + .expect('stdout', /\$ eggctl stop \[BASEDIR] /) + .expect('code', 0) + .end(); + }); +}); diff --git a/test/fixtures/cluster-config/app/router.js b/test/fixtures/cluster-config/app/router.js index 5a6c833..21c4989 100644 --- a/test/fixtures/cluster-config/app/router.js +++ b/test/fixtures/cluster-config/app/router.js @@ -1,7 +1,5 @@ -'use strict'; - module.exports = app => { - app.get('/', function* () { + app.get('/', async function() { this.body = `hi, ${app.config.framework || 'egg'}`; }); }; diff --git a/test/fixtures/cluster-config/config/config.default.js b/test/fixtures/cluster-config/config/config.default.js index 98de4f0..dad1a31 100644 --- a/test/fixtures/cluster-config/config/config.default.js +++ b/test/fixtures/cluster-config/config/config.default.js @@ -1,5 +1,3 @@ -'use strict'; - exports.keys = '123456'; exports.logger = { diff --git a/test/fixtures/cluster-config/config/config.prod.js b/test/fixtures/cluster-config/config/config.prod.js index fa189b2..6c31593 100644 --- a/test/fixtures/cluster-config/config/config.prod.js +++ b/test/fixtures/cluster-config/config/config.prod.js @@ -1,7 +1,5 @@ -'use strict'; - exports.cluster = { listen: { port: 8000, - } -} + }, +}; diff --git a/test/fixtures/custom-node-dir/.node/bin/foo b/test/fixtures/custom-node-dir/.node/bin/foo new file mode 100644 index 0000000..5716ca5 --- /dev/null +++ b/test/fixtures/custom-node-dir/.node/bin/foo @@ -0,0 +1 @@ +bar diff --git a/test/fixtures/custom-node-dir/app/router.js b/test/fixtures/custom-node-dir/app/router.js new file mode 100644 index 0000000..540d6f1 --- /dev/null +++ b/test/fixtures/custom-node-dir/app/router.js @@ -0,0 +1,5 @@ +module.exports = app => { + app.get('/', async function() { + this.body = `hi, ${process.env.PATH}`; + }); +}; diff --git a/test/fixtures/custom-node-dir/config/config.default.js b/test/fixtures/custom-node-dir/config/config.default.js new file mode 100644 index 0000000..5f44376 --- /dev/null +++ b/test/fixtures/custom-node-dir/config/config.default.js @@ -0,0 +1 @@ +exports.keys = '123456'; diff --git a/test/fixtures/custom-node-dir/package.json b/test/fixtures/custom-node-dir/package.json new file mode 100644 index 0000000..78ad6bb --- /dev/null +++ b/test/fixtures/custom-node-dir/package.json @@ -0,0 +1,7 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + } +} diff --git a/test/fixtures/egg-app/config/config.default.js b/test/fixtures/egg-app/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/egg-app/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/egg-app/node_modules/egg/index.js b/test/fixtures/egg-app/node_modules/egg/index.js new file mode 100644 index 0000000..7a1482f --- /dev/null +++ b/test/fixtures/egg-app/node_modules/egg/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('../../../../../node_modules/egg'); diff --git a/test/fixtures/egg-app/node_modules/egg/package.json b/test/fixtures/egg-app/node_modules/egg/package.json new file mode 100644 index 0000000..034e266 --- /dev/null +++ b/test/fixtures/egg-app/node_modules/egg/package.json @@ -0,0 +1,4 @@ +{ + "name": "egg", + "version": "1.0.0" +} diff --git a/test/fixtures/egg-app/package.json b/test/fixtures/egg-app/package.json new file mode 100644 index 0000000..f833722 --- /dev/null +++ b/test/fixtures/egg-app/package.json @@ -0,0 +1,6 @@ +{ + "name": "example", + "dependencies": { + "egg": "^1.0.0" + } +} diff --git a/test/fixtures/egg-revert/config/config.default.js b/test/fixtures/egg-revert/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/egg-revert/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/egg-revert/node_modules/custom-framework/index.js b/test/fixtures/egg-revert/node_modules/custom-framework/index.js new file mode 100644 index 0000000..4dde6a1 --- /dev/null +++ b/test/fixtures/egg-revert/node_modules/custom-framework/index.js @@ -0,0 +1,24 @@ +const { Application: _Application, Agent, startCluster: originStartCluster } = require('egg'); + +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +function startCluster(...args) { + if (process.env.CUSTOM_ENV && !process.env.EGG_SERVER_ENV) { + console.log('## EGG_SERVER_ENV is not pass'); + console.log('## CUSTOM_ENV:', process.env.CUSTOM_ENV); + process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; + } + return originStartCluster(...args); +} + +module.exports = { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/egg-revert/node_modules/custom-framework/package.json b/test/fixtures/egg-revert/node_modules/custom-framework/package.json new file mode 100644 index 0000000..a9328f7 --- /dev/null +++ b/test/fixtures/egg-revert/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} \ No newline at end of file diff --git a/test/fixtures/egg-revert/package.json b/test/fixtures/egg-revert/package.json new file mode 100644 index 0000000..71801ee --- /dev/null +++ b/test/fixtures/egg-revert/package.json @@ -0,0 +1,11 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "egg": { + "framework": "custom-framework", + "revert": "CVE-2023-46809" + } +} diff --git a/test/fixtures/egg-scripts-config/app.js b/test/fixtures/egg-scripts-config/app.js new file mode 100644 index 0000000..36e010e --- /dev/null +++ b/test/fixtures/egg-scripts-config/app.js @@ -0,0 +1,11 @@ +const { scheduler } = require('node:timers/promises'); + +module.exports = app => { + if (process.env.ERROR) { + app.logger.error(new Error(process.env.ERROR)); + } + + app.beforeStart(async () => { + await scheduler.wait(parseInt(process.env.WAIT_TIME)); + }); +}; diff --git a/test/fixtures/egg-scripts-config/config/config.default.js b/test/fixtures/egg-scripts-config/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/egg-scripts-config/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/egg-scripts-config/node_modules/custom-framework/index.js b/test/fixtures/egg-scripts-config/node_modules/custom-framework/index.js new file mode 100644 index 0000000..4dde6a1 --- /dev/null +++ b/test/fixtures/egg-scripts-config/node_modules/custom-framework/index.js @@ -0,0 +1,24 @@ +const { Application: _Application, Agent, startCluster: originStartCluster } = require('egg'); + +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +function startCluster(...args) { + if (process.env.CUSTOM_ENV && !process.env.EGG_SERVER_ENV) { + console.log('## EGG_SERVER_ENV is not pass'); + console.log('## CUSTOM_ENV:', process.env.CUSTOM_ENV); + process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; + } + return originStartCluster(...args); +} + +module.exports = { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/egg-scripts-config/node_modules/custom-framework/package.json b/test/fixtures/egg-scripts-config/node_modules/custom-framework/package.json new file mode 100644 index 0000000..a9328f7 --- /dev/null +++ b/test/fixtures/egg-scripts-config/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} \ No newline at end of file diff --git a/test/fixtures/egg-scripts-config/package.json b/test/fixtures/egg-scripts-config/package.json new file mode 100644 index 0000000..36c1f05 --- /dev/null +++ b/test/fixtures/egg-scripts-config/package.json @@ -0,0 +1,15 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "egg": { + "framework": "custom-framework" + }, + "eggScriptsConfig": { + "ignore-stderr": true, + "daemon": true, + "workers": 1 + } +} diff --git a/test/fixtures/egg-scripts-node-options/app.js b/test/fixtures/egg-scripts-node-options/app.js new file mode 100644 index 0000000..3fa4a19 --- /dev/null +++ b/test/fixtures/egg-scripts-node-options/app.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = () => { + console.log('process.execArgv:', process.execArgv); + console.log('maxHeaderSize:', require('http').maxHeaderSize); +}; diff --git a/test/fixtures/egg-scripts-node-options/config/config.default.js b/test/fixtures/egg-scripts-node-options/config/config.default.js new file mode 100644 index 0000000..c997e00 --- /dev/null +++ b/test/fixtures/egg-scripts-node-options/config/config.default.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.keys = '123456'; diff --git a/test/fixtures/egg-scripts-node-options/package.json b/test/fixtures/egg-scripts-node-options/package.json new file mode 100644 index 0000000..3541f16 --- /dev/null +++ b/test/fixtures/egg-scripts-node-options/package.json @@ -0,0 +1,11 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "eggScriptsConfig": { + "workers": 1, + "node-options--max-http-header-size": "20000" + } +} diff --git a/test/fixtures/example/app.js b/test/fixtures/example/app.js new file mode 100644 index 0000000..1da2776 --- /dev/null +++ b/test/fixtures/example/app.js @@ -0,0 +1,4 @@ +module.exports = () => { + // --no-deprecation + new Buffer('aaa'); +}; diff --git a/test/fixtures/example/app/router.js b/test/fixtures/example/app/router.js index 425da8a..2ca255f 100644 --- a/test/fixtures/example/app/router.js +++ b/test/fixtures/example/app/router.js @@ -1,11 +1,13 @@ -'use strict'; - module.exports = app => { - app.get('/', function* () { + app.get('/', async function() { this.body = `hi, ${app.config.framework || 'egg'}`; }); - app.get('/env', function* () { + app.get('/env', async function() { this.body = app.config.env + ', ' + app.config.pre; }); + + app.get('/path', async function() { + this.body = process.env.PATH; + }); }; diff --git a/test/fixtures/example/node_modules/custom-framework/index.js b/test/fixtures/example/node_modules/custom-framework/index.js index 50b41a0..e153588 100644 --- a/test/fixtures/example/node_modules/custom-framework/index.js +++ b/test/fixtures/example/node_modules/custom-framework/index.js @@ -1,11 +1,8 @@ -'use strict'; +const { Application: _Application, Agent, startCluster: originStartCluster } = require('egg'); -const egg = require('../../../../../node_modules/egg'); - -const originStartCluster = egg.startCluster; - -module.exports = Object.assign(egg, { - Application: class CustomApplication extends egg.Application { +module.exports = { + Agent, + Application: class CustomApplication extends _Application { get [Symbol.for('egg#eggPath')]() { return __dirname; } @@ -17,5 +14,5 @@ module.exports = Object.assign(egg, { process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; } return originStartCluster(...args); - } -}); + }, +}; diff --git a/test/fixtures/example/node_modules/yadan/index.js b/test/fixtures/example/node_modules/yadan/index.js index 5ce51a8..ddc49a7 100644 --- a/test/fixtures/example/node_modules/yadan/index.js +++ b/test/fixtures/example/node_modules/yadan/index.js @@ -1,13 +1,11 @@ -'use strict'; +const { Application: _Application, Agent, startCluster } = require('egg'); -const egg = require('../../../../../node_modules/egg'); - -const originStartCluster = egg.startCluster; - -module.exports = Object.assign(egg, { - Application: class CustomApplication extends egg.Application { +module.exports = { + Agent, + startCluster, + Application: class CustomApplication extends _Application { get [Symbol.for('egg#eggPath')]() { return __dirname; } }, -}); +}; diff --git a/test/fixtures/ipc-bin/start.js b/test/fixtures/ipc-bin/start.js new file mode 100644 index 0000000..2b858ca --- /dev/null +++ b/test/fixtures/ipc-bin/start.js @@ -0,0 +1,37 @@ +'use strict'; + +const co = require('co'); + +const BaseStartCommand = require('../../../lib/cmd/start'); + +class StartCommand extends BaseStartCommand { + * run(context) { + yield super.run(context); + const child = this.child; + child.on('message', msg => { + if (msg && msg.action === 'egg-ready') { + console.log('READY!!!'); + } + }); + } +} + +const start = new StartCommand(); + +co(function* () { + yield start.run({ + argv: { + framework: 'custom-framework', + _: [ process.env.BASE_DIR ], + workers: 2, + title: 'egg-server-example', + }, + cwd: process.env.BASE_DIR, + // FIXME: overide run argv so execArgvObj is missing + // execArgv: [], + env: { + PATH: process.env.PATH, + }, + }); +}); + diff --git a/test/fixtures/pkg-config-esm/config/config.default.js b/test/fixtures/pkg-config-esm/config/config.default.js new file mode 100644 index 0000000..45b1a89 --- /dev/null +++ b/test/fixtures/pkg-config-esm/config/config.default.js @@ -0,0 +1,7 @@ +export default { + keys: '123456', + logger: { + level: 'WARN', + consoleLevel: 'WARN', + }, +}; diff --git a/test/fixtures/pkg-config-esm/config/plugin.js b/test/fixtures/pkg-config-esm/config/plugin.js new file mode 100644 index 0000000..5f7239c --- /dev/null +++ b/test/fixtures/pkg-config-esm/config/plugin.js @@ -0,0 +1,146 @@ +export default { + // enable plugins + + /** + * app global Error Handling + * @member {Object} Plugin#onerror + * @property {Boolean} enable - `true` by default + */ + onerror: { + enable: false, + package: 'egg-onerror', + path: 'xxxxx', + }, + + /** + * session + * @member {Object} Plugin#session + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + session: { + enable: false, + package: 'egg-session', + path: 'xxxxx', + }, + + /** + * i18n + * @member {Object} Plugin#i18n + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + i18n: { + enable: false, + package: 'egg-i18n', + path: 'xxxxx', + }, + + /** + * file and dir watcher + * @member {Object} Plugin#watcher + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + watcher: { + enable: false, + package: 'egg-watcher', + path: 'xxxxx', + }, + + /** + * multipart + * @member {Object} Plugin#multipart + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + multipart: { + enable: false, + package: 'egg-multipart', + path: 'xxxxx', + }, + + /** + * security middlewares and extends + * @member {Object} Plugin#security + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + security: { + enable: false, + package: 'egg-security', + path: 'xxxxx', + }, + + /** + * local development helper + * @member {Object} Plugin#development + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + development: { + enable: false, + package: 'egg-development', + path: 'xxxxx', + }, + + /** + * logger file rotator + * @member {Object} Plugin#logrotator + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + logrotator: { + enable: false, + package: 'egg-logrotator', + path: 'xxxxx', + }, + + /** + * schedule tasks + * @member {Object} Plugin#schedule + * @property {Boolean} enable - `true` by default + * @since 2.7.0 + */ + schedule: { + enable: false, + package: 'egg-schedule', + path: 'xxxxx', + }, + + /** + * `app/public` dir static serve + * @member {Object} Plugin#static + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + static: { + enable: false, + package: 'egg-static', + path: 'xxxxx', + }, + + /** + * jsonp support for egg + * @member {Function} Plugin#jsonp + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + jsonp: { + enable: false, + package: 'egg-jsonp', + path: 'xxxxx', + }, + + /** + * view plugin + * @member {Function} Plugin#view + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + view: { + enable: false, + package: 'egg-view', + path: 'xxxxx', + }, +}; diff --git a/test/fixtures/pkg-config-esm/inject1.js b/test/fixtures/pkg-config-esm/inject1.js new file mode 100644 index 0000000..025593f --- /dev/null +++ b/test/fixtures/pkg-config-esm/inject1.js @@ -0,0 +1 @@ +console.log('@@@ inject script1'); diff --git a/test/fixtures/pkg-config-esm/inject2.js b/test/fixtures/pkg-config-esm/inject2.js new file mode 100644 index 0000000..ebb4122 --- /dev/null +++ b/test/fixtures/pkg-config-esm/inject2.js @@ -0,0 +1 @@ +console.log('@@@ inject script2'); diff --git a/test/fixtures/pkg-config-esm/node_modules/custom-framework/index.js b/test/fixtures/pkg-config-esm/node_modules/custom-framework/index.js new file mode 100644 index 0000000..28ee048 --- /dev/null +++ b/test/fixtures/pkg-config-esm/node_modules/custom-framework/index.js @@ -0,0 +1,19 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Application as _Application, Agent, startCluster } from 'egg'; + +const EGG_PATH = Symbol.for('egg#eggPath'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +export { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/pkg-config-esm/node_modules/custom-framework/package.json b/test/fixtures/pkg-config-esm/node_modules/custom-framework/package.json new file mode 100644 index 0000000..04d2690 --- /dev/null +++ b/test/fixtures/pkg-config-esm/node_modules/custom-framework/package.json @@ -0,0 +1,11 @@ +{ + "name": "custom-framework-esm", + "version": "1.0.0", + "dependencies": { + "egg": "*" + }, + "type": "module", + "egg": { + "framework": true + } +} diff --git a/test/fixtures/pkg-config-esm/node_modules/inject/index.js b/test/fixtures/pkg-config-esm/node_modules/inject/index.js new file mode 100644 index 0000000..ae5a6c9 --- /dev/null +++ b/test/fixtures/pkg-config-esm/node_modules/inject/index.js @@ -0,0 +1 @@ +console.log('@@@ inject script!'); diff --git a/test/fixtures/pkg-config-esm/node_modules/inject/package.json b/test/fixtures/pkg-config-esm/node_modules/inject/package.json new file mode 100644 index 0000000..f751e13 --- /dev/null +++ b/test/fixtures/pkg-config-esm/node_modules/inject/package.json @@ -0,0 +1,4 @@ +{ + "name": "inject", + "type": "module" +} diff --git a/test/fixtures/pkg-config-esm/package.json b/test/fixtures/pkg-config-esm/package.json new file mode 100644 index 0000000..51b26db --- /dev/null +++ b/test/fixtures/pkg-config-esm/package.json @@ -0,0 +1,14 @@ +{ + "name": "example-esm", + "version": "1.0.0", + "egg": { + "framework": "custom-framework" + }, + "eggScriptsConfig": { + "require": [ + "./inject1.js", + "inject" + ] + }, + "type": "module" +} diff --git a/test/fixtures/pkg-config-sourcemap/config/config.default.js b/test/fixtures/pkg-config-sourcemap/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/pkg-config-sourcemap/config/plugin.js b/test/fixtures/pkg-config-sourcemap/config/plugin.js new file mode 100644 index 0000000..18b9986 --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/config/plugin.js @@ -0,0 +1,148 @@ +'use strict'; + +module.exports = { + // enable plugins + + /** + * app global Error Handling + * @member {Object} Plugin#onerror + * @property {Boolean} enable - `true` by default + */ + onerror: { + enable: false, + package: 'egg-onerror', + path: 'xxxxx', + }, + + /** + * session + * @member {Object} Plugin#session + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + session: { + enable: false, + package: 'egg-session', + path: 'xxxxx', + }, + + /** + * i18n + * @member {Object} Plugin#i18n + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + i18n: { + enable: false, + package: 'egg-i18n', + path: 'xxxxx', + }, + + /** + * file and dir watcher + * @member {Object} Plugin#watcher + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + watcher: { + enable: false, + package: 'egg-watcher', + path: 'xxxxx', + }, + + /** + * multipart + * @member {Object} Plugin#multipart + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + multipart: { + enable: false, + package: 'egg-multipart', + path: 'xxxxx', + }, + + /** + * security middlewares and extends + * @member {Object} Plugin#security + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + security: { + enable: false, + package: 'egg-security', + path: 'xxxxx', + }, + + /** + * local development helper + * @member {Object} Plugin#development + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + development: { + enable: false, + package: 'egg-development', + path: 'xxxxx', + }, + + /** + * logger file rotator + * @member {Object} Plugin#logrotator + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + logrotator: { + enable: false, + package: 'egg-logrotator', + path: 'xxxxx', + }, + + /** + * schedule tasks + * @member {Object} Plugin#schedule + * @property {Boolean} enable - `true` by default + * @since 2.7.0 + */ + schedule: { + enable: false, + package: 'egg-schedule', + path: 'xxxxx', + }, + + /** + * `app/public` dir static serve + * @member {Object} Plugin#static + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + static: { + enable: false, + package: 'egg-static', + path: 'xxxxx', + }, + + /** + * jsonp support for egg + * @member {Function} Plugin#jsonp + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + jsonp: { + enable: false, + package: 'egg-jsonp', + path: 'xxxxx', + }, + + /** + * view plugin + * @member {Function} Plugin#view + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + view: { + enable: false, + package: 'egg-view', + path: 'xxxxx', + }, +}; diff --git a/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/index.js b/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/index.js new file mode 100644 index 0000000..5da602c --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/index.js @@ -0,0 +1,15 @@ +const { Application: _Application, Agent, startCluster } = require('egg'); + +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +module.exports = { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/package.json b/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/package.json new file mode 100644 index 0000000..073be53 --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} diff --git a/test/fixtures/pkg-config-sourcemap/node_modules/inject/index.js b/test/fixtures/pkg-config-sourcemap/node_modules/inject/index.js new file mode 100644 index 0000000..ae5a6c9 --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/node_modules/inject/index.js @@ -0,0 +1 @@ +console.log('@@@ inject script!'); diff --git a/test/fixtures/pkg-config-sourcemap/package.json b/test/fixtures/pkg-config-sourcemap/package.json new file mode 100644 index 0000000..a35c104 --- /dev/null +++ b/test/fixtures/pkg-config-sourcemap/package.json @@ -0,0 +1,11 @@ +{ + "name": "pkg-config-sourcemap", + "version": "1.0.0", + "egg": { + "typescript": true, + "framework": "custom-framework" + }, + "eggScriptsConfig": { + "sourcemap": false + } +} diff --git a/test/fixtures/pkg-config/config/config.default.js b/test/fixtures/pkg-config/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/pkg-config/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/pkg-config/config/plugin.js b/test/fixtures/pkg-config/config/plugin.js new file mode 100644 index 0000000..18b9986 --- /dev/null +++ b/test/fixtures/pkg-config/config/plugin.js @@ -0,0 +1,148 @@ +'use strict'; + +module.exports = { + // enable plugins + + /** + * app global Error Handling + * @member {Object} Plugin#onerror + * @property {Boolean} enable - `true` by default + */ + onerror: { + enable: false, + package: 'egg-onerror', + path: 'xxxxx', + }, + + /** + * session + * @member {Object} Plugin#session + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + session: { + enable: false, + package: 'egg-session', + path: 'xxxxx', + }, + + /** + * i18n + * @member {Object} Plugin#i18n + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + i18n: { + enable: false, + package: 'egg-i18n', + path: 'xxxxx', + }, + + /** + * file and dir watcher + * @member {Object} Plugin#watcher + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + watcher: { + enable: false, + package: 'egg-watcher', + path: 'xxxxx', + }, + + /** + * multipart + * @member {Object} Plugin#multipart + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + multipart: { + enable: false, + package: 'egg-multipart', + path: 'xxxxx', + }, + + /** + * security middlewares and extends + * @member {Object} Plugin#security + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + security: { + enable: false, + package: 'egg-security', + path: 'xxxxx', + }, + + /** + * local development helper + * @member {Object} Plugin#development + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + development: { + enable: false, + package: 'egg-development', + path: 'xxxxx', + }, + + /** + * logger file rotator + * @member {Object} Plugin#logrotator + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + logrotator: { + enable: false, + package: 'egg-logrotator', + path: 'xxxxx', + }, + + /** + * schedule tasks + * @member {Object} Plugin#schedule + * @property {Boolean} enable - `true` by default + * @since 2.7.0 + */ + schedule: { + enable: false, + package: 'egg-schedule', + path: 'xxxxx', + }, + + /** + * `app/public` dir static serve + * @member {Object} Plugin#static + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + static: { + enable: false, + package: 'egg-static', + path: 'xxxxx', + }, + + /** + * jsonp support for egg + * @member {Function} Plugin#jsonp + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + jsonp: { + enable: false, + package: 'egg-jsonp', + path: 'xxxxx', + }, + + /** + * view plugin + * @member {Function} Plugin#view + * @property {Boolean} enable - `true` by default + * @since 1.0.0 + */ + view: { + enable: false, + package: 'egg-view', + path: 'xxxxx', + }, +}; diff --git a/test/fixtures/pkg-config/inject1.js b/test/fixtures/pkg-config/inject1.js new file mode 100644 index 0000000..004fc72 --- /dev/null +++ b/test/fixtures/pkg-config/inject1.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('@@@ inject script1'); diff --git a/test/fixtures/pkg-config/inject2.js b/test/fixtures/pkg-config/inject2.js new file mode 100644 index 0000000..eea4a92 --- /dev/null +++ b/test/fixtures/pkg-config/inject2.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('@@@ inject script2'); diff --git a/test/fixtures/pkg-config/node_modules/custom-framework/index.js b/test/fixtures/pkg-config/node_modules/custom-framework/index.js new file mode 100644 index 0000000..5da602c --- /dev/null +++ b/test/fixtures/pkg-config/node_modules/custom-framework/index.js @@ -0,0 +1,15 @@ +const { Application: _Application, Agent, startCluster } = require('egg'); + +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +module.exports = { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/pkg-config/node_modules/custom-framework/package.json b/test/fixtures/pkg-config/node_modules/custom-framework/package.json new file mode 100644 index 0000000..073be53 --- /dev/null +++ b/test/fixtures/pkg-config/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} diff --git a/test/fixtures/pkg-config/node_modules/inject/index.js b/test/fixtures/pkg-config/node_modules/inject/index.js new file mode 100644 index 0000000..ae5a6c9 --- /dev/null +++ b/test/fixtures/pkg-config/node_modules/inject/index.js @@ -0,0 +1 @@ +console.log('@@@ inject script!'); diff --git a/test/fixtures/pkg-config/package.json b/test/fixtures/pkg-config/package.json new file mode 100644 index 0000000..772b332 --- /dev/null +++ b/test/fixtures/pkg-config/package.json @@ -0,0 +1,13 @@ +{ + "name": "example", + "version": "1.0.0", + "egg": { + "framework": "custom-framework" + }, + "eggScriptsConfig": { + "require": [ + "./inject1.js", + "inject" + ] + } +} diff --git a/test/fixtures/status/app.js b/test/fixtures/status/app.js new file mode 100644 index 0000000..36e010e --- /dev/null +++ b/test/fixtures/status/app.js @@ -0,0 +1,11 @@ +const { scheduler } = require('node:timers/promises'); + +module.exports = app => { + if (process.env.ERROR) { + app.logger.error(new Error(process.env.ERROR)); + } + + app.beforeStart(async () => { + await scheduler.wait(parseInt(process.env.WAIT_TIME)); + }); +}; diff --git a/test/fixtures/status/config/config.default.js b/test/fixtures/status/config/config.default.js new file mode 100644 index 0000000..dad1a31 --- /dev/null +++ b/test/fixtures/status/config/config.default.js @@ -0,0 +1,6 @@ +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/status/node_modules/custom-framework/index.js b/test/fixtures/status/node_modules/custom-framework/index.js new file mode 100644 index 0000000..4dde6a1 --- /dev/null +++ b/test/fixtures/status/node_modules/custom-framework/index.js @@ -0,0 +1,24 @@ +const { Application: _Application, Agent, startCluster: originStartCluster } = require('egg'); + +const EGG_PATH = Symbol.for('egg#eggPath'); + +class Application extends _Application { + get [EGG_PATH]() { + return __dirname; + } +} + +function startCluster(...args) { + if (process.env.CUSTOM_ENV && !process.env.EGG_SERVER_ENV) { + console.log('## EGG_SERVER_ENV is not pass'); + console.log('## CUSTOM_ENV:', process.env.CUSTOM_ENV); + process.env.EGG_SERVER_ENV = process.env.CUSTOM_ENV; + } + return originStartCluster(...args); +} + +module.exports = { + Application, + Agent, + startCluster, +}; diff --git a/test/fixtures/status/node_modules/custom-framework/package.json b/test/fixtures/status/node_modules/custom-framework/package.json new file mode 100644 index 0000000..a9328f7 --- /dev/null +++ b/test/fixtures/status/node_modules/custom-framework/package.json @@ -0,0 +1,7 @@ +{ + "name": "custom-framework", + "version": "1.0.0", + "dependencies": { + "egg": "*" + } +} \ No newline at end of file diff --git a/test/fixtures/status/package.json b/test/fixtures/status/package.json new file mode 100644 index 0000000..5fc5476 --- /dev/null +++ b/test/fixtures/status/package.json @@ -0,0 +1,10 @@ +{ + "name": "example", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "egg": { + "framework": "custom-framework" + } +} diff --git a/test/fixtures/stop-timeout/app.js b/test/fixtures/stop-timeout/app.js new file mode 100644 index 0000000..691386e --- /dev/null +++ b/test/fixtures/stop-timeout/app.js @@ -0,0 +1,8 @@ +'use strict'; + +const sleep = require('mz-modules/sleep'); +module.exports = app => { + app.beforeClose(function* () { + yield sleep(6000); + }); +}; diff --git a/test/fixtures/stop-timeout/app/router.js b/test/fixtures/stop-timeout/app/router.js new file mode 100644 index 0000000..2ca255f --- /dev/null +++ b/test/fixtures/stop-timeout/app/router.js @@ -0,0 +1,13 @@ +module.exports = app => { + app.get('/', async function() { + this.body = `hi, ${app.config.framework || 'egg'}`; + }); + + app.get('/env', async function() { + this.body = app.config.env + ', ' + app.config.pre; + }); + + app.get('/path', async function() { + this.body = process.env.PATH; + }); +}; diff --git a/test/fixtures/stop-timeout/config/config.default.js b/test/fixtures/stop-timeout/config/config.default.js new file mode 100644 index 0000000..98de4f0 --- /dev/null +++ b/test/fixtures/stop-timeout/config/config.default.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.keys = '123456'; + +exports.logger = { + level: 'WARN', + consoleLevel: 'WARN', +}; diff --git a/test/fixtures/stop-timeout/package.json b/test/fixtures/stop-timeout/package.json new file mode 100644 index 0000000..4da9169 --- /dev/null +++ b/test/fixtures/stop-timeout/package.json @@ -0,0 +1,7 @@ +{ + "name": "stop-timeout", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + } +} diff --git a/test/fixtures/subdir-as-basedir/base-dir/app/router.js b/test/fixtures/subdir-as-basedir/base-dir/app/router.js new file mode 100644 index 0000000..2ca255f --- /dev/null +++ b/test/fixtures/subdir-as-basedir/base-dir/app/router.js @@ -0,0 +1,13 @@ +module.exports = app => { + app.get('/', async function() { + this.body = `hi, ${app.config.framework || 'egg'}`; + }); + + app.get('/env', async function() { + this.body = app.config.env + ', ' + app.config.pre; + }); + + app.get('/path', async function() { + this.body = process.env.PATH; + }); +}; diff --git a/test/fixtures/subdir-as-basedir/base-dir/config/config.default.js b/test/fixtures/subdir-as-basedir/base-dir/config/config.default.js new file mode 100644 index 0000000..c997e00 --- /dev/null +++ b/test/fixtures/subdir-as-basedir/base-dir/config/config.default.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.keys = '123456'; diff --git a/test/fixtures/subdir-as-basedir/base-dir/package.json b/test/fixtures/subdir-as-basedir/base-dir/package.json new file mode 100644 index 0000000..37ddf6c --- /dev/null +++ b/test/fixtures/subdir-as-basedir/base-dir/package.json @@ -0,0 +1,3 @@ +{ + "name": "base-dir" +} \ No newline at end of file diff --git a/test/fixtures/subdir-as-basedir/package.json b/test/fixtures/subdir-as-basedir/package.json new file mode 100644 index 0000000..fbbc6a0 --- /dev/null +++ b/test/fixtures/subdir-as-basedir/package.json @@ -0,0 +1,3 @@ +{ + "name": "subdir-as-basedir" +} \ No newline at end of file diff --git a/test/fixtures/trace-warnings/app.js b/test/fixtures/trace-warnings/app.js new file mode 100644 index 0000000..3925891 --- /dev/null +++ b/test/fixtures/trace-warnings/app.js @@ -0,0 +1,14 @@ +const EventEmitter = require('events'); + +module.exports = () => { + console.log('app loaded'); + const event = new EventEmitter(); + event.setMaxListeners(1); + + // --trace-warnings test about MaxListenersExceededWarning + event.on('xx', () => {}); + event.on('xx', () => {}); + + // will not effect --no-deprecation argv + new Buffer('aaa'); +}; diff --git a/test/fixtures/trace-warnings/package.json b/test/fixtures/trace-warnings/package.json new file mode 100644 index 0000000..dfc4b17 --- /dev/null +++ b/test/fixtures/trace-warnings/package.json @@ -0,0 +1,4 @@ +{ + "name": "trace-warnings", + "version": "1.0.0" +} diff --git a/test/fixtures/ts-pkg/app/controller/home.ts b/test/fixtures/ts-pkg/app/controller/home.ts new file mode 100644 index 0000000..5424538 --- /dev/null +++ b/test/fixtures/ts-pkg/app/controller/home.ts @@ -0,0 +1,15 @@ +import { Controller } from 'egg'; + +export default class AppController extends Controller { + public index() { + try { + throw new Error('some err'); + } catch (err: any) { + this.ctx.logger.error(err); + this.ctx.body = { + msg: err.message, + stack: err.stack, + }; + } + } +} diff --git a/test/fixtures/ts-pkg/app/router.js b/test/fixtures/ts-pkg/app/router.js new file mode 100644 index 0000000..bece6e7 --- /dev/null +++ b/test/fixtures/ts-pkg/app/router.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = app => { + const { router, controller } = app; + router.get('/', controller.home.index); +}; diff --git a/test/fixtures/ts-pkg/config/config.default.js b/test/fixtures/ts-pkg/config/config.default.js new file mode 100644 index 0000000..c997e00 --- /dev/null +++ b/test/fixtures/ts-pkg/config/config.default.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.keys = '123456'; diff --git a/test/fixtures/ts-pkg/package.json b/test/fixtures/ts-pkg/package.json new file mode 100644 index 0000000..cd48b79 --- /dev/null +++ b/test/fixtures/ts-pkg/package.json @@ -0,0 +1,14 @@ +{ + "name": "ts-pkg", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "egg": { + "typescript": true + }, + "scripts": { + "build": "./../../../node_modules/.bin/tsc", + "windows-build": "call ../../../node_modules/.bin/tsc.cmd" + } +} diff --git a/test/fixtures/ts-pkg/tsconfig.json b/test/fixtures/ts-pkg/tsconfig.json new file mode 100644 index 0000000..f5bd4ba --- /dev/null +++ b/test/fixtures/ts-pkg/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "strict": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "charset": "utf8", + "allowJs": false, + "pretty": true, + "noEmitOnError": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "strictPropertyInitialization": false, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "inlineSourceMap": true, + "importHelpers": true + }, + "exclude": [ + "app/public", + "app/views" + ] +} \ No newline at end of file diff --git a/test/fixtures/ts/app/controller/home.ts b/test/fixtures/ts/app/controller/home.ts new file mode 100644 index 0000000..5424538 --- /dev/null +++ b/test/fixtures/ts/app/controller/home.ts @@ -0,0 +1,15 @@ +import { Controller } from 'egg'; + +export default class AppController extends Controller { + public index() { + try { + throw new Error('some err'); + } catch (err: any) { + this.ctx.logger.error(err); + this.ctx.body = { + msg: err.message, + stack: err.stack, + }; + } + } +} diff --git a/test/fixtures/ts/app/router.js b/test/fixtures/ts/app/router.js new file mode 100644 index 0000000..bece6e7 --- /dev/null +++ b/test/fixtures/ts/app/router.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = app => { + const { router, controller } = app; + router.get('/', controller.home.index); +}; diff --git a/test/fixtures/ts/config/config.default.js b/test/fixtures/ts/config/config.default.js new file mode 100644 index 0000000..c997e00 --- /dev/null +++ b/test/fixtures/ts/config/config.default.js @@ -0,0 +1,3 @@ +'use strict'; + +exports.keys = '123456'; diff --git a/test/fixtures/ts/package.json b/test/fixtures/ts/package.json new file mode 100644 index 0000000..26b2e6f --- /dev/null +++ b/test/fixtures/ts/package.json @@ -0,0 +1,11 @@ +{ + "name": "ts", + "version": "1.0.0", + "dependencies": { + "egg": "^1.0.0" + }, + "scripts": { + "build": "./../../../node_modules/.bin/tsc", + "windows-build": "call ../../../node_modules/.bin/tsc.cmd" + } +} diff --git a/test/fixtures/ts/tsconfig.json b/test/fixtures/ts/tsconfig.json new file mode 100644 index 0000000..2b11d3f --- /dev/null +++ b/test/fixtures/ts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2017", + "module": "commonjs", + "strict": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowJs": false, + "pretty": true, + "noEmitOnError": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "strictPropertyInitialization": false, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "inlineSourceMap": true, + "importHelpers": true + }, + "exclude": [ + "app/public", + "app/views" + ] +} diff --git a/test/start.test.js b/test/start.test.js deleted file mode 100644 index b07fb9b..0000000 --- a/test/start.test.js +++ /dev/null @@ -1,340 +0,0 @@ -'use strict'; - -const path = require('path'); -const assert = require('assert'); -const fs = require('mz/fs'); -const sleep = require('mz-modules/sleep'); -const rimraf = require('mz-modules/rimraf'); -const mkdirp = require('mz-modules/mkdirp'); -const coffee = require('coffee'); -const homedir = require('node-homedir'); -const httpclient = require('urllib'); -const mm = require('mm'); -const utils = require('./utils'); - -describe('test/start.test.js', () => { - const eggBin = require.resolve('../bin/egg-scripts.js'); - const fixturePath = path.join(__dirname, 'fixtures/example'); - const homePath = homedir(); - const logDir = path.join(homePath, 'logs/example'); - const waitTime = '10s'; - - afterEach(() => mm.restore); - - describe('start without daemon', () => { - describe('full path', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.includes('--title=egg-server-example')); - assert(app.stdout.includes('"title":"egg-server-example"')); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - assert(app.stdout.includes('app_worker#2:')); - assert(!app.stdout.includes('app_worker#3:')); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('relative path', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', path.relative(process.cwd(), fixturePath) ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('without baseDir', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2' ], { cwd: fixturePath }); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('--framework', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--framework=yadan', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/yadan started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, yadan'); - }); - }); - - describe('--title', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=egg-test', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.includes('--title=egg-test')); - assert(app.stdout.includes('"title":"egg-test"')); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - assert(app.stdout.includes('app_worker#2:')); - assert(!app.stdout.includes('app_worker#3:')); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('--port', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--port=7002', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7002/)); - const result = yield httpclient.request('http://127.0.0.1:7002'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('process.env.PORT', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ], { env: Object.assign({}, process.env, { PORT: 7002 }) }); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7002/)); - const result = yield httpclient.request('http://127.0.0.1:7002'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - - describe('--env', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', '--env=pre', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001/env'); - assert(result.data.toString() === 'pre, true'); - }); - }); - - describe('custom env', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - mm(process.env, 'CUSTOM_ENV', 'pre'); - app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.includes('## EGG_SERVER_ENV is not pass')); - assert(app.stdout.includes('## CUSTOM_ENV: pre')); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001/env'); - assert(result.data.toString() === 'pre, true'); - }); - }); - - describe('read cluster config', () => { - let app; - const fixturePath = path.join(__dirname, 'fixtures/cluster-config'); - - before(function* () { - yield utils.cleanup(fixturePath); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/egg started on http:\/\/127\.0\.0\.1:8000/)); - assert(!app.stdout.includes('app_worker#3:')); - const result = yield httpclient.request('http://127.0.0.1:8000'); - assert(result.data.toString() === 'hi, egg'); - }); - }); - }); - - describe('start with daemon', () => { - let app; - - before(function* () { - yield utils.cleanup(fixturePath); - yield rimraf(logDir); - yield mkdirp(logDir); - yield fs.writeFile(path.join(logDir, 'master-stdout.log'), 'just for test'); - yield fs.writeFile(path.join(logDir, 'master-stderr.log'), 'just for test'); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should start', function* () { - app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', '--port=7002', fixturePath ]); - // app.debug(); - app.expect('code', 0); - - yield sleep(waitTime); - - assert(app.stdout.match(/starting egg.*example/)); - assert(app.stdout.match(/egg started on http:\/\/127\.0\.0\.1:7002/)); - - // master log - const stdout = yield fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8'); - const stderr = yield fs.readFile(path.join(logDir, 'master-stderr.log'), 'utf-8'); - assert(stderr === ''); - assert(stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7002/)); - - // should rotate log - const fileList = yield fs.readdir(logDir); - assert(fileList.some(name => name.match(/master-stdout\.log\.\d+\.\d+/))); - assert(fileList.some(name => name.match(/master-stderr\.log\.\d+\.\d+/))); - - const result = yield httpclient.request('http://127.0.0.1:7002'); - assert(result.data.toString() === 'hi, egg'); - }); - }); -}); diff --git a/test/start.test.ts b/test/start.test.ts new file mode 100644 index 0000000..395586e --- /dev/null +++ b/test/start.test.ts @@ -0,0 +1,885 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { strict as assert } from 'node:assert'; +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import { createServer } from 'node:http'; +import { once } from 'node:events'; +import coffee from 'coffee'; +import { request } from 'urllib'; +import { mm, restore } from 'mm'; +import { exists } from 'utility'; +import { cleanup, replaceWeakRefMessage, Coffee } from './utils.js'; +import { isWindows, getSourceFilename } from '../src/helper.js'; + +const version = parseInt(process.version.split('.')[0].substring(1)); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('test/start.test.ts', () => { + const eggBin = getSourceFilename('../bin/run.js'); + const fixturePath = path.join(__dirname, 'fixtures/example'); + const homePath = path.join(__dirname, 'fixtures/home'); + const logDir = path.join(homePath, 'logs'); + const waitTime = 10000; + + before(async () => { + await fs.mkdir(homePath, { recursive: true }); + }); + after(async () => { + await fs.rm(homePath, { force: true, recursive: true }); + }); + beforeEach(() => mm(process.env, 'MOCK_HOME_DIR', homePath)); + afterEach(restore); + + describe('start without daemon', () => { + describe('read pkgInfo on CommonJS', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/pkg-config'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should --require work', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', '--require=./inject2.js' ], { + cwd: fixturePath, + }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /@@@ inject script\!/); + assert.match(app.stdout, /@@@ inject script1/); + assert.match(app.stdout, /@@@ inject script2/); + }); + + it('inject incorrect script', async () => { + const script = './inject3.js'; + app = coffee.fork(eggBin, [ 'start', '--workers=1', `--require=${script}` ], { + cwd: fixturePath, + }) as Coffee; + // app.debug(); + await scheduler.wait(waitTime); + assert.match(app.stderr, /Cannot find module/); + app.expect('code', 1); + }); + }); + + describe('read pkgInfo on ESM', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/pkg-config-esm'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should --require work', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', '--require=./inject2.js' ], { + cwd: fixturePath, + }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /@@@ inject script\!/); + assert.match(app.stdout, /@@@ inject script1/); + assert.match(app.stdout, /@@@ inject script2/); + }); + + it('inject incorrect script', async () => { + const script = './inject3.js'; + app = coffee.fork(eggBin, [ 'start', '--workers=1', `--require=${script}` ], { cwd: fixturePath }) as Coffee; + // app.debug(); + await scheduler.wait(waitTime); + assert.match(app.stderr, /Cannot find module/); + app.expect('code', 1); + }); + }); + + describe('sourcemap default value should respect eggScriptConfig', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/pkg-config-sourcemap'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should not enable sourcemap-support', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1' ], { cwd: fixturePath }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + assert.doesNotMatch(app.stdout, /--require .*\/node_modules\/.*source-map-support/); + }); + }); + + describe('full path', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + afterEach(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]) as Coffee; + app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + // assert(!app.stdout.includes('DeprecationWarning:')); + assert(app.stdout.includes('--title=egg-server-example')); + assert(app.stdout.includes('"title":"egg-server-example"')); + assert.match(app.stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7001/); + assert.match(app.stdout, /app_worker#2:/); + assert.doesNotMatch(app.stdout, /app_worker#3:/); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + + it('should start --trace-warnings work', async () => { + app = coffee.fork(eggBin, [ + 'start', '--workers=1', path.join(__dirname, 'fixtures/trace-warnings'), + ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + // assert.match(app.stderr, /MaxListenersExceededWarning:/); + // assert.match(app.stderr, /app.js:10:9/); // should had trace + assert.doesNotMatch(app.stdout, /DeprecationWarning:/); + }); + + it.skip('should get ready', async () => { + app = coffee.fork(path.join(__dirname, './fixtures/ipc-bin/start.js'), [], { + env: { + BASE_DIR: fixturePath, + PATH: process.env.PATH, + }, + }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.includes('READY!!!')); + assert(app.stdout.includes('--title=egg-server-example')); + assert(app.stdout.includes('"title":"egg-server-example"')); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + assert(app.stdout.includes('app_worker#2:')); + assert(!app.stdout.includes('app_worker#3:')); + }); + }); + + describe('child exit with 1', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should emit spawn error', async () => { + const server = createServer(() => {}); + server.listen(7007); + + app = coffee.fork(eggBin, [ 'start', '--port=7007', '--workers=2', fixturePath ]) as Coffee; + + await scheduler.wait(waitTime); + server.close(); + assert.equal(app.code, 1); + }); + }); + + describe('relative path', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', path.relative(process.cwd(), fixturePath) ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('without baseDir', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2' ], { cwd: fixturePath }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('--framework', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--framework=yadan', '--workers=2', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/yadan started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, yadan'); + }); + }); + + describe('--title', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=egg-test', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.includes('--title=egg-test')); + assert(app.stdout.includes('"title":"egg-test"')); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + assert(app.stdout.includes('app_worker#2:')); + assert(!app.stdout.includes('app_worker#3:')); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('--port', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--port=7002', '--workers=2', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7002/)); + const result = await request('http://127.0.0.1:7002'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('process.env.PORT', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ], { + env: Object.assign({}, process.env, { PORT: 7002 }), + }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7002/); + const result = await request('http://127.0.0.1:7002'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('--env', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--env=pre', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001/env'); + assert.equal(result.data.toString(), 'pre, true'); + }); + }); + + describe('custom env', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + mm(process.env, 'CUSTOM_ENV', 'pre'); + app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.includes('## EGG_SERVER_ENV is not pass')); + assert(app.stdout.includes('## CUSTOM_ENV: pre')); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + let result = await request('http://127.0.0.1:7001/env'); + assert.equal(result.data.toString(), 'pre, true'); + result = await request('http://127.0.0.1:7001/path'); + const appBinPath = path.join(fixturePath, 'node_modules/.bin'); + assert(result.data.toString().startsWith(`${appBinPath}${path.delimiter}`)); + }); + }); + + describe('--stdout --stderr', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + await fs.rm(logDir, { force: true, recursive: true }); + await fs.rm(path.join(fixturePath, 'start-fail'), { force: true, recursive: true }); + await fs.mkdir(logDir, { recursive: true }); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + await fs.rm(path.join(fixturePath, 'stdout.log'), { force: true }); + await fs.rm(path.join(fixturePath, 'stderr.log'), { force: true }); + await fs.rm(path.join(fixturePath, 'start-fail'), { force: true, recursive: true }); + }); + + it('should start', async () => { + const stdout = path.join(fixturePath, 'stdout.log'); + const stderr = path.join(fixturePath, 'stderr.log'); + app = coffee.fork(eggBin, [ + 'start', '--workers=1', '--daemon', `--stdout=${stdout}`, `--stderr=${stderr}`, fixturePath, + ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + let content = await fs.readFile(stdout, 'utf-8'); + assert.match(content, /custom-framework started on http:\/\/127\.0\.0\.1:7001/); + + content = await fs.readFile(stderr, 'utf-8'); + assert.equal(content, ''); + }); + + it('should start with insecurity --stderr argument', async () => { + const cwd = path.join(__dirname, 'fixtures/status'); + mm(process.env, 'ERROR', 'error message'); + + const stdout = path.join(fixturePath, 'start-fail/stdout.log'); + const stderr = path.join(fixturePath, 'start-fail/stderr.log'); + const malicious = path.join(fixturePath, 'start-fail/malicious'); + app = coffee.fork(eggBin, [ + 'start', '--workers=1', '--daemon', `--stdout=${stdout}`, + `--stderr=${stderr}; touch ${malicious}`, + cwd, + ]) as Coffee; + // app.debug(); + + await scheduler.wait(waitTime); + + const content = await fs.readFile(stdout, 'utf-8'); + assert(!content.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + let stats = await exists(stderr); + assert(!stats); + stats = await exists(malicious); + assert(!stats); + }); + }); + + describe('--node', () => { + let app: Coffee; + + beforeEach(async () => { + await cleanup(fixturePath); + }); + + beforeEach(async () => { + app && app.proc && app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + describe('daemon', () => { + it('should start', async () => { + app = coffee.fork(eggBin, [ + 'start', '--daemon', '--framework=yadan', '--workers=2', `--node=${process.execPath}`, fixturePath, + ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/yadan started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, yadan'); + }); + + it('should error if node path invalid', async () => { + app = coffee.fork(eggBin, [ + 'start', '--daemon', '--framework=yadan', '--workers=2', '--node=invalid', fixturePath, + ]) as Coffee; + // app.debug(); + app.expect('code', 1); + + await scheduler.wait(3000); + assert.match(app.stderr, /spawn invalid ENOENT/); + }); + }); + + describe('not daemon', () => { + it('should start', async () => { + app = coffee.fork(eggBin, [ + 'start', '--framework=yadan', '--workers=2', `--node=${process.execPath}`, fixturePath, + ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/yadan started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, yadan'); + }); + + it('should error if node path invalid', async () => { + app = coffee.fork(eggBin, [ + 'start', '--framework=yadan', '--workers=2', '--node=invalid', fixturePath, + ]) as Coffee; + // app.debug(); + app.expect('code', 1); + + await scheduler.wait(3000); + assert.match(app.stderr, /spawn invalid ENOENT/); + }); + }); + }); + + describe('read cluster config', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/cluster-config'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/egg started on http:\/\/127\.0\.0\.1:8000/)); + assert(!app.stdout.includes('app_worker#3:')); + const result = await request('http://127.0.0.1:8000'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('read eggScriptsConfig', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/egg-scripts-node-options'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', fixturePath ]) as Coffee; + app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /maxHeaderSize: 20000/); + }); + }); + + describe('read egg.revert', () => { + if (version !== 20) return; + if (isWindows) return; + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/egg-revert'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /SECURITY WARNING: Reverting CVE-2023-46809: Marvin attack on PKCS#1 padding/); + }); + }); + + describe('subDir as baseDir', () => { + let app: Coffee; + const rootDir = path.join(__dirname, '..'); + const subDir = path.join(__dirname, 'fixtures/subdir-as-basedir/base-dir'); + + before(async () => { + await cleanup(rootDir); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(rootDir); + }); + + it('should start', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=2', subDir ], { cwd: rootDir }) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/egg started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + }); + + describe('auto set custom node dir to PATH', () => { + let app: Coffee; + let fixturePath: string; + + before(async () => { + fixturePath = path.join(__dirname, 'fixtures/custom-node-dir'); + await cleanup(fixturePath); + }); + + after(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should start', async () => { + const expectPATH = [ + path.join(fixturePath, 'node_modules/.bin'), + path.join(fixturePath, '.node/bin'), + ].join(path.delimiter) + path.delimiter; + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--port=7002', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /egg started on http:\/\/127\.0\.0\.1:7002/); + assert(!app.stdout.includes('app_worker#3:')); + const result = await request('http://127.0.0.1:7002'); + assert(result.data.toString().startsWith(`hi, ${expectPATH}`)); + }); + }); + + describe('kill command', () => { + let app: Coffee; + + before(async () => { + await cleanup(fixturePath); + }); + + after(async () => { + await cleanup(fixturePath); + }); + + it('should wait child process exit', async () => { + app = coffee.fork(eggBin, [ 'start', '--port=7007', '--workers=2', fixturePath ]) as Coffee; + await scheduler.wait(waitTime); + const exitEvent = once(app.proc, 'exit'); + app.proc.kill('SIGTERM'); + const [ code ] = await exitEvent; + if (isWindows) { + assert(code === null); + } else { + assert.equal(code, 0); + } + }); + }); + }); + + describe('start with daemon', () => { + let cwd: string; + beforeEach(async () => { + if (cwd) { + await cleanup(cwd); + } + await fs.rm(logDir, { force: true, recursive: true }); + await fs.mkdir(logDir, { recursive: true }); + await fs.writeFile(path.join(logDir, 'master-stdout.log'), 'just for test'); + await fs.writeFile(path.join(logDir, 'master-stderr.log'), 'just for test'); + }); + + afterEach(async () => { + await coffee.fork(eggBin, [ 'stop', cwd ]) + // .debug() + .end(); + await cleanup(cwd); + }); + + it('should start custom-framework', async () => { + cwd = fixturePath; + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', '--port=7002', cwd ]) + // .debug() + .expect('stdout', /Starting custom-framework application/) + .expect('stdout', /custom-framework started on http:\/\/127\.0\.0\.1:7002/) + .expect('code', 0) + .end(); + + // master log + const stdout = await fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8'); + const stderr = await fs.readFile(path.join(logDir, 'master-stderr.log'), 'utf-8'); + assert(stderr === ''); + assert.match(stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7002/); + + // should rotate log + const fileList = await fs.readdir(logDir); + // console.log(fileList); + assert(fileList.some(name => name.match(/master-stdout\.log\.\d+\.\d+/))); + assert(fileList.some(name => name.match(/master-stderr\.log\.\d+\.\d+/))); + + const result = await request('http://127.0.0.1:7002'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + + it('should start default egg', async () => { + cwd = path.join(__dirname, 'fixtures/egg-app'); + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', cwd ]) + // .debug() + .expect('stdout', /Starting egg application/) + .expect('stdout', /egg started on http:\/\/127\.0\.0\.1:7001/) + .expect('code', 0) + .end(); + }); + }); + + describe('check status', () => { + let cwd: string; + beforeEach(() => { + cwd = path.join(__dirname, 'fixtures/status'); + }); + + after(async () => { + await coffee.fork(eggBin, [ 'stop', cwd ]) + // .debug() + .end(); + await cleanup(cwd); + }); + + it('should status check success, exit with 0', async () => { + mm(process.env, 'WAIT_TIME', 3000); + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1' ], { cwd }) + // .debug() + .expect('stdout', /Wait Start: 2.../) + .expect('stdout', /custom-framework started/) + .expect('code', 0) + .end(); + }); + + it('should status check fail `--ignore-stderr`, exit with 0', async () => { + mm(process.env, 'WAIT_TIME', 3000); + mm(process.env, 'ERROR', 'error message'); + const app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1', '--ignore-stderr' ], { cwd }); + // app.debug(); + // TODO: find a windows replacement for tail command + if (!isWindows) { + app.expect('stderr', /nodejs.Error: error message/); + } + await app.expect('stderr', /Start got error, see /) + .expect('code', 0) + .end(); + }); + + it('should status check fail `--ignore-stderr` in package.json, exit with 0', async () => { + cwd = path.join(__dirname, 'fixtures/egg-scripts-config'); + mm(process.env, 'WAIT_TIME', 3000); + mm(process.env, 'ERROR', 'error message'); + + const app = coffee.fork(eggBin, [ 'start' ], { cwd }); + // app.debug(); + // TODO: find a windows replacement for tail command + if (!isWindows) { + app.expect('stderr', /nodejs.Error: error message/); + } + await app.expect('stderr', /Start got error, see /) + .expect('code', 0) + .end(); + }); + + it('should status check fail, exit with 1', async () => { + mm(process.env, 'WAIT_TIME', 3000); + mm(process.env, 'ERROR', 'error message'); + + const app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1' ], { cwd }); + // app.debug(); + // TODO: find a windows replacement for tail command + if (!isWindows) { + app.expect('stderr', /nodejs.Error: error message/); + } + await app.expect('stderr', /Start got error, see /) + .expect('stderr', /Got error when startup/) + .expect('code', 1) + .end(); + }); + + it('should status check timeout and exit with code 1', async () => { + mm(process.env, 'WAIT_TIME', 10000); + + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=1', '--timeout=5000' ], { cwd }) + // .debug() + .expect('stdout', /Wait Start: 1.../) + .expect('stderr', /Start failed, 5s timeout/) + .expect('code', 1) + .end(); + }); + }); +}); diff --git a/test/stop.test.js b/test/stop.test.js deleted file mode 100644 index 22f0470..0000000 --- a/test/stop.test.js +++ /dev/null @@ -1,162 +0,0 @@ -'use strict'; - -const path = require('path'); -const assert = require('assert'); -const fs = require('mz/fs'); -const sleep = require('mz-modules/sleep'); -const rimraf = require('mz-modules/rimraf'); -const coffee = require('coffee'); -const homedir = require('node-homedir'); -const httpclient = require('urllib'); -const utils = require('./utils'); - -describe('test/stop.test.js', () => { - const eggBin = require.resolve('../bin/egg-scripts.js'); - const fixturePath = path.join(__dirname, 'fixtures/example'); - const homePath = homedir(); - const logDir = path.join(homePath, 'logs/example'); - const waitTime = '10s'; - - describe('stop without daemon', () => { - let app; - let killer; - - beforeEach(function* () { - yield utils.cleanup(fixturePath); - app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - yield sleep(waitTime); - - assert(app.stderr === ''); - assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - - afterEach(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - describe('full path', () => { - it('should stop', function* () { - killer = coffee.fork(eggBin, [ 'stop', fixturePath ]); - killer.debug(); - killer.expect('code', 0); - - // yield killer.end(); - yield sleep(waitTime); - - // make sure is kill not auto exist - assert(!app.stdout.includes('exist by env')); - - assert(app.stdout.includes('[master] receive signal SIGTERM, closing')); - assert(app.stdout.includes('[master] exit with code:0')); - assert(app.stdout.includes('[app_worker] exit with code:0')); - // assert(app.stdout.includes('[agent_worker] exit with code:0')); - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stdout.match(/got master pid \["\d+\"\]/i)); - }); - }); - - describe('relative path', () => { - it('should stop', function* () { - killer = coffee.fork(eggBin, [ 'stop', path.relative(process.cwd(), fixturePath) ]); - killer.debug(); - killer.expect('code', 0); - - // yield killer.end(); - yield sleep(waitTime); - - // make sure is kill not auto exist - assert(!app.stdout.includes('exist by env')); - - assert(app.stdout.includes('[master] receive signal SIGTERM, closing')); - assert(app.stdout.includes('[master] exit with code:0')); - assert(app.stdout.includes('[app_worker] exit with code:0')); - // assert(app.stdout.includes('[agent_worker] exit with code:0')); - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stdout.match(/got master pid \["\d+\"\]/i)); - }); - }); - - describe('without baseDir', () => { - it('should stop', function* () { - killer = coffee.fork(eggBin, [ 'stop' ], { cwd: fixturePath }); - killer.debug(); - killer.expect('code', 0); - - // yield killer.end(); - yield sleep(waitTime); - - // make sure is kill not auto exist - assert(!app.stdout.includes('exist by env')); - - assert(app.stdout.includes('[master] receive signal SIGTERM, closing')); - assert(app.stdout.includes('[master] exit with code:0')); - assert(app.stdout.includes('[app_worker] exit with code:0')); - // assert(app.stdout.includes('[agent_worker] exit with code:0')); - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stdout.match(/got master pid \["\d+\"\]/i)); - }); - }); - }); - - describe('stop with daemon', () => { - let app; - let killer; - - before(function* () { - yield utils.cleanup(fixturePath); - yield rimraf(logDir); - app = coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', fixturePath ]); - // app.debug(); - app.expect('code', 0); - yield sleep('10s'); - - const result = yield httpclient.request('http://127.0.0.1:7001'); - assert(result.data.toString() === 'hi, egg'); - }); - - after(function* () { - app.proc.kill('SIGTERM'); - yield utils.cleanup(fixturePath); - }); - - it('should stop', function* () { - killer = coffee.fork(eggBin, [ 'stop', fixturePath ]); - killer.debug(); - killer.expect('code', 0); - - yield killer.end(); - yield sleep(waitTime); - - // master log - const stdout = yield fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8'); - - assert(stdout.includes('[master] receive signal SIGTERM, closing')); - assert(stdout.includes('[master] exit with code:0')); - assert(stdout.includes('[app_worker] exit with code:0')); - // assert(stdout.includes('[agent_worker] exit with code:0')); - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stdout.match(/got master pid \["\d+\"\]/i)); - }); - }); - - describe('stop with not exist', () => { - let killer; - - it('should work', function* () { - yield utils.cleanup(fixturePath); - killer = coffee.fork(eggBin, [ 'stop', fixturePath ]); - killer.debug(); - killer.expect('code', 0); - - yield sleep('5s'); - - assert(killer.stdout.includes(`[egg-scripts] stopping egg application at ${fixturePath}`)); - assert(killer.stderr.includes('can\'t detect any running egg process')); - }); - }); -}); diff --git a/test/stop.test.ts b/test/stop.test.ts new file mode 100644 index 0000000..4896b0a --- /dev/null +++ b/test/stop.test.ts @@ -0,0 +1,375 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { strict as assert } from 'node:assert'; +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import coffee from 'coffee'; +import { request } from 'urllib'; +import { mm, restore } from 'mm'; +import { cleanup, replaceWeakRefMessage, Coffee } from './utils.js'; +import { isWindows, getSourceFilename } from '../src/helper.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('test/stop.test.ts', () => { + const eggBin = getSourceFilename('../bin/run.js'); + const fixturePath = path.join(__dirname, 'fixtures/example'); + const timeoutPath = path.join(__dirname, 'fixtures/stop-timeout'); + const homePath = path.join(__dirname, 'fixtures/home'); + const logDir = path.join(homePath, 'logs'); + const waitTime = 10000; + + before(async () => { + await fs.mkdir(homePath, { recursive: true }); + }); + after(async () => { + await fs.rm(homePath, { force: true, recursive: true }); + }); + beforeEach(() => mm(process.env, 'MOCK_HOME_DIR', homePath)); + afterEach(restore); + + describe('stop without daemon', () => { + let app: Coffee; + let killer: Coffee; + + beforeEach(async () => { + await cleanup(fixturePath); + app = coffee.fork(eggBin, [ 'start', '--workers=2', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/custom-framework started on http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + }); + + afterEach(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should stop', async () => { + killer = coffee.fork(eggBin, [ 'stop', fixturePath ]) as Coffee; + // killer.debug(); + killer.expect('code', 0); + await killer.end(); + + // make sure is kill not auto exist + assert.doesNotMatch(app.stdout, /exist by env/); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert.match(app.stdout, /\[master] master is killed by signal SIGTERM, closing/); + assert.match(app.stdout, /\[master] exit with code:0/); + assert.match(app.stdout, /\[app_worker] exit with code:0/); + // assert(app.stdout.includes('[agent_worker] exit with code:0')); + } + + assert.match(killer.stdout, /stopping egg application/); + assert.match(killer.stdout, /got master pid \[\d+\]/); + }); + }); + + describe('stop with daemon', () => { + beforeEach(async () => { + await cleanup(fixturePath); + await fs.rm(logDir, { force: true, recursive: true }); + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2', fixturePath ]) + // .debug() + .expect('code', 0) + .end(); + + const result = await request('http://127.0.0.1:7001'); + assert(result.data.toString() === 'hi, egg'); + }); + afterEach(async () => { + await cleanup(fixturePath); + }); + + it('should stop', async () => { + await coffee.fork(eggBin, [ 'stop', fixturePath ]) + .debug() + .expect('stdout', /stopping egg application/) + .expect('stdout', /got master pid \[\d+\]/i) + .expect('code', 0) + .end(); + + // master log + const stdout = await fs.readFile(path.join(logDir, 'master-stdout.log'), 'utf-8'); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert.match(stdout, /\[master] master is killed by signal SIGTERM, closing/); + assert.match(stdout, /\[master] exit with code:0/); + assert.match(stdout, /\[app_worker] exit with code:0/); + } + + await coffee.fork(eggBin, [ 'stop', fixturePath ]) + .debug() + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); + }); + }); + + describe('stop with not exist', () => { + it('should work', async () => { + await cleanup(fixturePath); + await coffee.fork(eggBin, [ 'stop', fixturePath ]) + // .debug() + .expect('stdout', /stopping egg application/) + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); + }); + }); + + describe('stop --title', () => { + let app: Coffee; + let killer: Coffee; + + beforeEach(async () => { + await cleanup(fixturePath); + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=example', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001'); + assert(result.data.toString() === 'hi, egg'); + }); + + afterEach(async () => { + app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should stop only if the title matches exactly', async () => { + // Because of'exmaple'.inclues('exmap') === true,if egg-scripts <= 2.1.0 and you run `.. stop --title=exmap`,the process with 'title:example' will also be killed unexpectedly + await coffee.fork(eggBin, [ 'stop', '--title=examp', fixturePath ]) + // .debug() + .expect('stdout', /stopping egg application with --title=examp/) + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); + + // stop only if the title matches exactly + await coffee.fork(eggBin, [ 'stop', '--title=example', fixturePath ]) + // .debug() + .expect('stdout', /stopping egg application with --title=example/) + .expect('stdout', /got master pid \[/) + .expect('code', 0) + .end(); + }); + + it('should stop', async () => { + await coffee.fork(eggBin, [ 'stop', '--title=random', fixturePath ]) + .debug() + .expect('stdout', /stopping egg application with --title=random/) + .expect('stderr', /can't detect any running egg process/) + .expect('code', 0) + .end(); + + killer = coffee.fork(eggBin, [ 'stop', '--title=example' ], { cwd: fixturePath }) as Coffee; + killer.debug(); + // killer.expect('code', 0); + await killer.end(); + + // make sure is kill not auto exist + assert.doesNotMatch(app.stdout, /exist by env/); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert(app.stdout.includes('[master] master is killed by signal SIGTERM, closing')); + assert(app.stdout.includes('[master] exit with code:0')); + assert(app.stdout.includes('[app_worker] exit with code:0')); + // assert(app.stdout.includes('[agent_worker] exit with code:0')); + } + + assert(killer.stdout.includes('stopping egg application with --title=example')); + assert(killer.stdout.match(/got master pid \[\d+\]/i)); + }); + }); + + describe('stop all', () => { + let app: Coffee; + let app2: Coffee; + let killer: Coffee; + + beforeEach(async () => { + await cleanup(fixturePath); + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=example', fixturePath ]) as Coffee; + app.debug(); + app.expect('code', 0); + + app2 = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=test', '--port=7002', fixturePath ]) as Coffee; + app2.expect('code', 0); + + await scheduler.wait(10000); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001'); + assert.equal(result.data.toString(), 'hi, egg'); + + assert.equal(replaceWeakRefMessage(app2.stderr), ''); + assert.match(app2.stdout, /custom-framework started on http:\/\/127\.0\.0\.1:7002/); + const result2 = await request('http://127.0.0.1:7002'); + assert.equal(result2.data.toString(), 'hi, egg'); + }); + + afterEach(async () => { + app.proc.kill('SIGTERM'); + app2.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should stop', async () => { + killer = coffee.fork(eggBin, [ 'stop' ], { cwd: fixturePath }) as Coffee; + killer.debug(); + // killer.expect('code', 0); + await killer.end(); + + // make sure is kill not auto exist + assert(!app.stdout.includes('exist by env')); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert(app.stdout.includes('[master] master is killed by signal SIGTERM, closing')); + assert(app.stdout.includes('[master] exit with code:0')); + assert(app.stdout.includes('[app_worker] exit with code:0')); + // assert(app.stdout.includes('[agent_worker] exit with code:0')); + } + + assert(killer.stdout.includes('stopping egg application')); + assert(killer.stdout.match(/got master pid \[\d+,\d+\]/i)); + + assert(!app2.stdout.includes('exist by env')); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert(app2.stdout.includes('[master] master is killed by signal SIGTERM, closing')); + assert(app2.stdout.includes('[master] exit with code:0')); + assert(app2.stdout.includes('[app_worker] exit with code:0')); + } + }); + }); + + describe('stop all with timeout', function() { + let app: Coffee; + let killer: Coffee; + this.timeout(17000); + beforeEach(async () => { + await cleanup(timeoutPath); + app = coffee.fork(eggBin, [ 'start', '--workers=2', '--title=stop-timeout', timeoutPath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + // assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert(app.stdout.match(/http:\/\/127\.0\.0\.1:7001/)); + const result = await request('http://127.0.0.1:7001'); + assert(result.data.toString() === 'hi, egg'); + }); + + afterEach(async () => { + app.proc.kill('SIGTERM'); + await cleanup(timeoutPath); + }); + + it('should stop error without timeout', async () => { + killer = coffee.fork(eggBin, [ 'stop' ], { cwd: timeoutPath }) as Coffee; + killer.debug(); + killer.expect('code', 0); + await killer.end(); + await scheduler.wait(waitTime); + + // make sure is kill not auto exist + assert(!app.stdout.includes('exist by env')); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert(app.stdout.includes('[master] master is killed by signal SIGTERM, closing')); + assert(app.stdout.match(/app_worker#\d+:\d+ disconnect/)); + assert(app.stdout.match(/don't fork, because worker:\d+ will be kill soon/)); + } + + assert(killer.stdout.includes('stopping egg application')); + assert(killer.stdout.match(/got master pid \[\d+\]/i)); + }); + + it('should stop success', async () => { + killer = coffee.fork(eggBin, [ 'stop', '--timeout=10000' ], { cwd: timeoutPath }) as Coffee; + killer.debug(); + killer.expect('code', 0); + + // await killer.end(); + await scheduler.wait(waitTime); + + // make sure is kill not auto exist + assert(!app.stdout.includes('exist by env')); + + // no way to handle the SIGTERM signal in windows ? + if (!isWindows) { + assert(app.stdout.includes('[master] master is killed by signal SIGTERM, closing')); + assert(app.stdout.includes('[master] exit with code:0')); + assert(app.stdout.includes('[agent_worker] exit with code:0')); + } + + assert(killer.stdout.includes('stopping egg application')); + assert(killer.stdout.match(/got master pid \[\d+\]/i)); + }); + }); + + describe('stop with symlink', () => { + const baseDir = path.join(__dirname, 'fixtures/tmp'); + + beforeEach(async function() { + // if we can't create a symlink, skip the test + try { + await fs.symlink(fixturePath, baseDir, 'dir'); + } catch (err) { + // may get Error: EPERM: operation not permitted on windows + console.log(`test skiped, can't create symlink: ${err}`); + this.skip(); + } + + // *unix get the real path of symlink, but windows wouldn't + const appPathInRegexp = isWindows ? baseDir.replace(/\\/g, '\\\\') : fixturePath; + + await cleanup(fixturePath); + await fs.rm(logDir, { force: true, recursive: true }); + await coffee.fork(eggBin, [ 'start', '--daemon', '--workers=2' ], { cwd: baseDir }) + .debug() + .expect('stdout', new RegExp(`Starting custom-framework application at ${appPathInRegexp}`)) + .expect('code', 0) + .end(); + + await fs.rm(baseDir, { force: true, recursive: true }); + const result = await request('http://127.0.0.1:7001'); + assert(result.data.toString() === 'hi, egg'); + }); + afterEach(async () => { + await cleanup(fixturePath); + await fs.rm(baseDir, { force: true, recursive: true }); + }); + + it('should stop', async () => { + await fs.rm(baseDir, { force: true, recursive: true }); + await fs.symlink(path.join(__dirname, 'fixtures/status'), baseDir); + + await coffee.fork(eggBin, [ 'stop', baseDir ]) + .debug() + .expect('stdout', /stopping egg application/) + .expect('stdout', /got master pid \[\d+\]/i) + .expect('code', 0) + .end(); + }); + }); +}); diff --git a/test/ts.test.ts b/test/ts.test.ts new file mode 100644 index 0000000..771cc3a --- /dev/null +++ b/test/ts.test.ts @@ -0,0 +1,121 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { strict as assert } from 'node:assert'; +import fs from 'node:fs/promises'; +import cp from 'node:child_process'; +import { scheduler } from 'node:timers/promises'; +import coffee from 'coffee'; +import { request } from 'urllib'; +import { mm, restore } from 'mm'; +import { cleanup, replaceWeakRefMessage, Coffee } from './utils.js'; +import { isWindows, getSourceFilename } from '../src/helper.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +describe('test/ts.test.ts', () => { + const eggBin = getSourceFilename('../bin/run.js'); + const homePath = path.join(__dirname, 'fixtures/home'); + const waitTime = 5000; + let fixturePath: string; + + beforeEach(() => mm(process.env, 'MOCK_HOME_DIR', homePath)); + afterEach(restore); + + before(() => fs.mkdir(homePath, { recursive: true })); + after(() => fs.rm(homePath, { recursive: true, force: true })); + + describe('should display correct stack traces', () => { + let app: Coffee; + beforeEach(async () => { + fixturePath = path.join(__dirname, 'fixtures/ts'); + await cleanup(fixturePath); + const result = cp.spawnSync('npm', [ 'run', isWindows ? 'windows-build' : 'build' ], { + cwd: fixturePath, + shell: isWindows, + }); + assert.equal(result.stderr.toString(), ''); + }); + + afterEach(async () => { + app && app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('--ts', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', '--ts', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /egg started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001', { dataType: 'json' }); + // console.log(result.data); + assert(result.data.stack.includes(path.normalize('app/controller/home.ts:6:13'))); + }); + + it('--typescript', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', '--typescript', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /egg started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001', { dataType: 'json' }); + // console.log(result.data); + assert(result.data.stack.includes(path.normalize('app/controller/home.ts:6:13'))); + }); + + it('--sourcemap', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', '--sourcemap', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /egg started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001', { dataType: 'json' }); + // console.log(result.data); + assert(result.data.stack.includes(path.normalize('app/controller/home.ts:6:13'))); + }); + }); + + describe('pkg.egg.typescript', () => { + let app: Coffee; + beforeEach(async () => { + fixturePath = path.join(__dirname, 'fixtures/ts-pkg'); + await cleanup(fixturePath); + const result = cp.spawnSync('npm', [ 'run', isWindows ? 'windows-build' : 'build' ], { + cwd: fixturePath, + shell: isWindows, + }); + assert.equal(result.stderr.toString(), ''); + }); + + afterEach(async () => { + app && app.proc.kill('SIGTERM'); + await cleanup(fixturePath); + }); + + it('should got correct stack', async () => { + app = coffee.fork(eggBin, [ 'start', '--workers=1', fixturePath ]) as Coffee; + // app.debug(); + app.expect('code', 0); + + await scheduler.wait(waitTime); + + assert.equal(replaceWeakRefMessage(app.stderr), ''); + assert.match(app.stdout, /egg started on http:\/\/127\.0\.0\.1:7001/); + const result = await request('http://127.0.0.1:7001', { dataType: 'json' }); + console.log(result.data); + assert.match(result.data.stack, /home\.ts:6:13/); + // assert(result.data.stack.includes(path.normalize('app/controller/home.ts:6:13'))); + }); + }); +}); + diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index 3e97e7a..0000000 --- a/test/utils.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -const helper = require('../lib/helper'); -const sleep = require('mz-modules/sleep'); - -exports.cleanup = function* (baseDir) { - const processList = yield helper.findNodeProcess(x => x.cmd.includes(`"baseDir":"${baseDir}"`)); - - if (processList.length) { - console.log(`cleanup: ${processList.length} to kill`); - for (const item of processList) { - const pid = item.pid; - const cmd = item.cmd; - let type = 'unknown: ' + cmd; - if (cmd.includes('start-cluster')) { - type = 'master'; - } else if (cmd.includes('app_worker.js')) { - type = 'worker'; - } else if (cmd.includes('agent_worker.js')) { - type = 'agent'; - } - - try { - process.kill(pid, type === 'master' ? '' : 'SIGKILL'); - console.log(`cleanup ${type} ${pid}`); - } catch (err) { - console.log(`cleanup ${type} ${pid} got error ${err.code || err.message || err}`); - if (err.code !== 'ESRCH') { - throw err; - } - } - } - - yield sleep('5s'); - } -}; diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..60f1673 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,50 @@ +import { scheduler } from 'node:timers/promises'; +import { ChildProcess } from 'node:child_process'; +import { Coffee as _Coffee } from 'coffee'; +import { isWindows, findNodeProcess } from '../src/helper.js'; + +export type Coffee = _Coffee & { proc: ChildProcess, stderr: string, stdout: string, code?: number }; + +export async function cleanup(baseDir: string) { + const processList = await findNodeProcess(x => { + const dir = isWindows ? baseDir.replace(/\\/g, '\\\\') : baseDir; + const prefix = isWindows ? '\\"baseDir\\":\\"' : '"baseDir":"'; + return x.cmd.includes(`${prefix}${dir}`); + }); + + if (processList.length) { + console.log(`cleanup: ${processList.length} to kill`); + for (const item of processList) { + const pid = item.pid; + const cmd = item.cmd; + let type = 'unknown: ' + cmd; + if (cmd.includes('start-cluster')) { + type = 'master'; + } else if (cmd.includes('app_worker.js')) { + type = 'worker'; + } else if (cmd.includes('agent_worker.js')) { + type = 'agent'; + } + + try { + process.kill(pid, type === 'master' ? '' : 'SIGKILL'); + console.log(`cleanup ${type} ${pid}`); + } catch (err: any) { + console.log(`cleanup ${type} ${pid} got error ${err.code || err.message || err}`); + if (err.code !== 'ESRCH') { + throw err; + } + } + } + + await scheduler.wait(5000); + } +} + +export function replaceWeakRefMessage(stderr: string) { + // Using compatibility WeakRef and FinalizationRegistry\r\n + if (stderr.includes('Using compatibility WeakRef and FinalizationRegistry')) { + stderr = stderr.replace(/Using compatibility WeakRef and FinalizationRegistry[\r\n]*/g, ''); + } + return stderr; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +}