├── test
├── fixtures
│ ├── custom
│ │ ├── app
│ │ │ └── service
│ │ │ │ └── .gitkeep
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── override
│ │ ├── app
│ │ │ ├── service
│ │ │ │ └── .gitkeep
│ │ │ └── no-trigger
│ │ │ │ └── .gitkeep
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── development
│ │ ├── app
│ │ │ ├── assets
│ │ │ │ └── .gitkeep
│ │ │ ├── service
│ │ │ │ └── .gitkeep
│ │ │ ├── public
│ │ │ │ └── foo.js
│ │ │ └── router.js
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── not-reload
│ │ ├── app
│ │ │ └── service
│ │ │ │ └── .gitkeep
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── override-ignore
│ │ ├── app
│ │ │ ├── web
│ │ │ │ └── .gitkeep
│ │ │ └── public
│ │ │ │ └── .gitkeep
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── absolute
│ │ ├── package.json
│ │ └── config
│ │ │ └── config.default.js
│ ├── timing
│ │ ├── package.json
│ │ ├── config
│ │ │ └── config.js
│ │ ├── app
│ │ │ └── router.js
│ │ └── app.js
│ ├── delay-ready
│ │ ├── package.json
│ │ └── app.js
│ └── fast-ready
│ │ ├── package.json
│ │ ├── config
│ │ └── config.default.js
│ │ └── app.js
├── utils.js
├── not-reload.test.js
├── fast_ready_false.test.js
├── custom.test.js
├── absolute.test.js
├── process_mode_single.test.js
├── timing.test.js
├── development.test.js
└── override.test.js
├── .eslintignore
├── .eslintrc
├── .gitignore
├── app.js
├── .github
└── workflows
│ ├── release.yml
│ ├── nodejs.yml
│ └── pkg.pr.new.yml
├── config
└── config.default.js
├── lib
└── loader_trace.html
├── LICENSE
├── app
└── middleware
│ └── egg_loader_trace.js
├── package.json
├── agent.js
├── README.md
└── CHANGELOG.md
/test/fixtures/custom/app/service/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/override/app/service/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/development/app/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/development/app/service/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/not-reload/app/service/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/override-ignore/app/web/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/override/app/no-trigger/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/fixtures
2 | coverage
3 | .vscode
--------------------------------------------------------------------------------
/test/fixtures/override-ignore/app/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-egg"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/development/app/public/foo.js:
--------------------------------------------------------------------------------
1 | alert('bar');
2 |
--------------------------------------------------------------------------------
/test/fixtures/custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/override/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "override"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/absolute/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "absolute"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/not-reload/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "not-reload"
3 | }
--------------------------------------------------------------------------------
/test/fixtures/timing/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-ready"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/delay-ready/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "delay-ready"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/development/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "development"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/fast-ready/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-ready"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/override-ignore/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "override-ignore"
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/timing/config/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.keys = '11';
4 |
--------------------------------------------------------------------------------
/test/fixtures/development/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.keys = 'foo,bar';
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs/
2 | npm-debug.log
3 | node_modules/
4 | coverage/
5 | test/fixtures/**/run
6 | .DS_Store
7 | package-lock.json
8 |
--------------------------------------------------------------------------------
/test/fixtures/fast-ready/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.development = {
4 | fastReady: true,
5 | };
6 |
--------------------------------------------------------------------------------
/test/fixtures/custom/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.development = {
4 | reloadPattern: ['**', '!**/*.ts'],
5 | }
--------------------------------------------------------------------------------
/test/fixtures/not-reload/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.development = {
4 | reloadOnDebug: false,
5 | };
6 |
--------------------------------------------------------------------------------
/test/fixtures/override/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.development = {
4 | overrideDefault: true,
5 | watchDirs: [
6 | 'app/service',
7 | ]
8 | };
9 |
--------------------------------------------------------------------------------
/test/fixtures/timing/app/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = app => {
4 | app.get('/checkFile', async ctx => {
5 | ctx.body = ctx.app.checkFile;
6 | });
7 | };
8 |
--------------------------------------------------------------------------------
/test/fixtures/override-ignore/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | exports.development = {
4 | overrideIgnore: true,
5 | ignoreDirs: [
6 | 'app/public',
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | // if true, then don't need to wait at local development mode
3 | if (app.config.development.fastReady) {
4 | process.nextTick(() => app.ready(true));
5 | }
6 | app.config.coreMiddlewares.push('eggLoaderTrace');
7 | };
8 |
--------------------------------------------------------------------------------
/test/fixtures/development/app/router.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = app => {
4 | app.get('/foo.js', function* () {
5 | this.body = 'foo.js';
6 | });
7 |
8 | app.get('/foo', function* () {
9 | this.body = 'foo';
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | exports.escape = str => {
2 | return str
3 | .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
4 | .replace(/-/g, '\\x2d');
5 | };
6 |
7 | exports.sleep = ms => {
8 | return new Promise(resolve => {
9 | setTimeout(resolve, ms);
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/test/fixtures/absolute/config/config.default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | module.exports = appInfo => {
6 | return {
7 | keys: 'foo,bar',
8 | development: {
9 | watchDirs: [
10 | path.join(appInfo.baseDir, 'lib'),
11 | ],
12 | },
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/test/fixtures/delay-ready/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = app => {
4 | const done = app.readyCallback('delay 200ms');
5 | setTimeout(() => {
6 | app.logger.info('delayed 200ms done.');
7 | done();
8 | }, 200);
9 |
10 | app.ready(() => {
11 | app.logger.info('Server started.');
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/test/fixtures/fast-ready/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = app => {
4 | const done = app.readyCallback('delay 200ms');
5 | setTimeout(() => {
6 | app.logger.info('delayed 200ms done.');
7 | done();
8 | }, 200);
9 |
10 | app.ready(() => {
11 | app.logger.info('Server started.');
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release for 3.x
2 |
3 | on:
4 | push:
5 | branches: [ 3.x ]
6 |
7 | jobs:
8 | release:
9 | name: Node.js
10 | uses: eggjs/github-actions/.github/workflows/node-release.yml@master
11 | secrets:
12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
14 |
--------------------------------------------------------------------------------
/test/fixtures/timing/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | module.exports = app => {
7 | app.checkFile = {
8 | timing: fs.existsSync(path.join(__dirname, 'run/agent_timing_11111.json')),
9 | config: fs.existsSync(path.join(__dirname, 'run/application_config.json')),
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: CI for 3.x
2 |
3 | on:
4 | push:
5 | branches: [ 3.x ]
6 | pull_request:
7 | branches: [ 3.x ]
8 |
9 | jobs:
10 | Job:
11 | name: Node.js
12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master
13 | with:
14 | os: 'ubuntu-latest, macos-latest'
15 | version: '14, 16, 18, 20, 22'
16 | secrets:
17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
18 |
--------------------------------------------------------------------------------
/.github/workflows/pkg.pr.new.yml:
--------------------------------------------------------------------------------
1 | name: Publish Any Commit
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | build:
6 | runs-on: ubuntu-latest
7 |
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v4
11 |
12 | - run: corepack enable
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 20
16 |
17 | - name: Install dependencies
18 | run: npm install
19 |
20 | - name: Build
21 | run: npm run prepublishOnly --if-present
22 |
23 | - run: npx pkg-pr-new publish
24 |
--------------------------------------------------------------------------------
/config/config.default.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @member Config#development
3 | * @property {Array} watchDirs - dirs needed watch, when files under these change, application will reload, use relative path
4 | * @property {Array} ignoreDirs - dirs don't need watch, including subdirectories, use relative path
5 | * @property {Boolean} fastReady - don't wait all plugins ready, default is false.
6 | * @property {Boolean} reloadOnDebug - whether reload on debug, default is true.
7 | * @property {Boolean} overrideDefault - whether override default watchDirs, default is false.
8 | * @property {Boolean} overrideIgnore - whether override default ignoreDirs, default is false.
9 | * @property {Array|String} reloadPattern - whether to reload, use https://github.com/sindresorhus/multimatch
10 | */
11 | exports.development = {
12 | watchDirs: [],
13 | ignoreDirs: [],
14 | fastReady: false,
15 | reloadOnDebug: true,
16 | overrideDefault: false,
17 | overrideIgnore: false,
18 | reloadPattern: undefined,
19 | };
20 |
--------------------------------------------------------------------------------
/test/not-reload.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 | const mm = require('egg-mock');
4 | const { escape, sleep } = require('./utils');
5 |
6 | describe('test/not-reload.test.js', () => {
7 | let app;
8 | before(() => {
9 | mm.env('local');
10 | mm(process.env, 'EGG_DEBUG', true);
11 | app = mm.cluster({
12 | baseDir: 'not-reload',
13 | opt: {
14 | execArgv: [ '--inspect' ],
15 | },
16 | });
17 | return app.ready();
18 | });
19 | after(() => app.close());
20 | afterEach(mm.restore);
21 | // for debounce
22 | afterEach(() => sleep(500));
23 |
24 | it('should not reload', async () => {
25 | const filepath = path.join(__dirname, 'fixtures/not-reload/app/service/a.js');
26 | await fs.writeFile(filepath, '');
27 | await sleep(1000);
28 |
29 | await fs.unlink(filepath);
30 | app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`)));
31 | });
32 | });
33 |
34 |
--------------------------------------------------------------------------------
/test/fast_ready_false.test.js:
--------------------------------------------------------------------------------
1 | const mm = require('egg-mock');
2 | const { sleep } = require('./utils');
3 |
4 | describe('fastReady = false', () => {
5 | let app;
6 | beforeEach(() => {
7 | mm(process.env, 'NODE_ENV', 'development');
8 | });
9 | afterEach(() => app.close());
10 | afterEach(mm.restore);
11 | // for debounce
12 | afterEach(() => sleep(500));
13 |
14 | it('should fast ready by default', async () => {
15 | app = mm.cluster({
16 | baseDir: 'delay-ready',
17 | });
18 | await app.ready();
19 | // We need to wait for log written, because app.logger.info is async.
20 | await sleep(100);
21 |
22 | app.expect('stdout', /Server started./);
23 | });
24 |
25 | it('should not fast ready if config.development.fastReady is false', async () => {
26 | app = mm.cluster({
27 | baseDir: 'fast-ready',
28 | });
29 | await app.ready();
30 | // We need to wait for log written, because app.logger.info is async.
31 | await sleep(300);
32 |
33 | app.expect('stdout', /delayed 200ms done./);
34 | app.expect('stdout', /Server started./);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/lib/loader_trace.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Alibaba Group Holding Limited and other contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/custom.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises');
2 | const path = require('node:path');
3 | const mm = require('egg-mock');
4 | const { escape, sleep } = require('./utils');
5 |
6 | describe('test/custom.test.js', () => {
7 | let app;
8 | before(() => {
9 | mm.env('local');
10 | app = mm.cluster({
11 | baseDir: 'custom',
12 | });
13 | app.debug();
14 | return app.ready();
15 | });
16 | after(() => app.close());
17 | afterEach(mm.restore);
18 | // for debounce
19 | afterEach(() => sleep(500));
20 |
21 | it('should reload with custom detect', async () => {
22 | let filepath;
23 | filepath = path.join(__dirname, 'fixtures/custom/app/service/a.js');
24 | await fs.writeFile(filepath, '');
25 | await sleep(5000);
26 |
27 | await fs.unlink(filepath);
28 | app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
29 |
30 | filepath = path.join(__dirname, 'fixtures/custom/app/service/b.ts');
31 | await fs.writeFile(filepath, '');
32 | await sleep(5000);
33 |
34 | await fs.unlink(filepath);
35 | app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/app/middleware/egg_loader_trace.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const fs = require('node:fs/promises');
3 | const { readJSON } = require('utility');
4 |
5 | module.exports = (_, app) => {
6 | return async (ctx, next) => {
7 | if (ctx.path !== '/__loader_trace__') return await next();
8 | const template = await fs.readFile(path.join(__dirname, '../../lib/loader_trace.html'), 'utf8');
9 | const data = await loadTimingData(app);
10 | ctx.body = template.replace('{{placeholder}}', JSON.stringify(data));
11 | };
12 | };
13 |
14 | async function loadTimingData(app) {
15 | const rundir = app.config.rundir;
16 | const files = await fs.readdir(rundir);
17 | const data = [];
18 | for (const file of files) {
19 | if (!/^(agent|application)_timing/.test(file)) continue;
20 | const json = await readJSON(path.join(rundir, file));
21 | const isAgent = /^agent/.test(file);
22 | for (const item of json) {
23 | if (isAgent) {
24 | item.type = 'agent';
25 | } else {
26 | item.type = `app_${item.pid}`;
27 | }
28 | item.pid = String(item.pid);
29 | item.range = [ item.start, item.end ];
30 | item.title = `${item.type}(${item.index})`;
31 | data.push(item);
32 | }
33 | }
34 | return data;
35 | }
36 |
--------------------------------------------------------------------------------
/test/absolute.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 | const mm = require('egg-mock');
4 | const { sleep } = require('./utils');
5 |
6 | describe('test/absolute.test.js', () => {
7 | let app;
8 | before(async () => {
9 | await fs.rm(path.join(__dirname, 'fixtures/absolute/lib'), { force: true, recursive: true });
10 |
11 | // FIXME: ONLY WATCH EXIST DIR
12 | const filepath = path.join(__dirname, 'fixtures/absolute/lib/a/b.js');
13 | await fs.mkdir(path.dirname(filepath), { recursive: true });
14 | await fs.writeFile(filepath, '');
15 |
16 | mm.env('local');
17 | app = mm.cluster({
18 | baseDir: 'absolute',
19 | debug: true,
20 | });
21 | return app.ready();
22 | });
23 | after(() => app.close());
24 | afterEach(mm.restore);
25 | // for debounce
26 | afterEach(() => sleep(500));
27 |
28 | it('should reload at absolute path', async () => {
29 | const filepath = path.join(__dirname, 'fixtures/absolute/lib/a/b.js');
30 | await fs.mkdir(path.dirname(filepath), { recursive: true });
31 | console.log(`write file to ${filepath}`);
32 | await fs.writeFile(filepath, 'console.log(1);');
33 | await sleep(5000);
34 |
35 | await fs.unlink(filepath);
36 | app.expect('stdout', /reload worker because .*?b\.js/);
37 | });
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "egg-development",
3 | "version": "3.0.2",
4 | "description": "development tool for egg",
5 | "eggPlugin": {
6 | "name": "development",
7 | "env": [
8 | "local"
9 | ],
10 | "dependencies": [
11 | "watcher"
12 | ]
13 | },
14 | "keywords": [
15 | "egg",
16 | "plugin",
17 | "egg-plugin",
18 | "eggPlugin"
19 | ],
20 | "dependencies": {
21 | "debounce": "^1.1.0",
22 | "multimatch": "^5.0.0",
23 | "utility": "^2.4.0"
24 | },
25 | "devDependencies": {
26 | "@types/node": "^22.10.2",
27 | "egg": "3",
28 | "egg-bin": "6",
29 | "egg-mock": "5",
30 | "eslint": "8",
31 | "eslint-config-egg": "12",
32 | "supertest": "^3.4.2"
33 | },
34 | "engines": {
35 | "node": ">=14.0.0"
36 | },
37 | "scripts": {
38 | "test": "npm run lint -- --fix && npm run test-local",
39 | "test-local": "egg-bin test --ts false",
40 | "cov": "egg-bin cov --ts false",
41 | "lint": "eslint .",
42 | "ci": "npm run lint && npm run cov"
43 | },
44 | "repository": {
45 | "type": "git",
46 | "url": "git+https://github.com/eggjs/development.git"
47 | },
48 | "files": [
49 | "app",
50 | "config",
51 | "lib",
52 | "agent.js",
53 | "app.js"
54 | ],
55 | "bugs": "https://github.com/eggjs/egg/issues",
56 | "homepage": "https://github.com/eggjs/development#readme",
57 | "author": "jtyjty99999",
58 | "license": "MIT"
59 | }
60 |
--------------------------------------------------------------------------------
/test/process_mode_single.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 | const assert = require('node:assert');
4 | const request = require('supertest');
5 | const mm = require('egg-mock');
6 | const { sleep } = require('./utils');
7 |
8 | describe('test/process_mode_single.test.js', () => {
9 | let app;
10 | before(async () => {
11 | app = await require('egg').start({
12 | env: 'local',
13 | baseDir: path.join(__dirname, 'fixtures/development'),
14 | plugins: {
15 | development: {
16 | enable: true,
17 | path: path.join(__dirname, '..'),
18 | },
19 | },
20 | });
21 | });
22 | after(() => app.close());
23 | afterEach(mm.restore);
24 |
25 | it('should not reload', async () => {
26 | let warn = false;
27 | mm(app.agent.logger, 'warn', msg => {
28 | if (msg.includes('reload worker')) warn = true;
29 | });
30 | await request(app.callback()).get('/foo')
31 | .expect(200)
32 | .expect('foo');
33 | const filepath = path.join(__dirname, 'fixtures/development/app/service/a.js');
34 | await fs.writeFile(filepath, '');
35 | await sleep(1000);
36 |
37 | await request(app.callback()).get('/foo')
38 | .expect(200)
39 | .expect('foo');
40 |
41 | await fs.unlink(filepath);
42 |
43 | await request(app.callback()).get('/foo')
44 | .expect(200)
45 | .expect('foo');
46 |
47 | assert(!warn);
48 | });
49 | });
50 |
51 |
--------------------------------------------------------------------------------
/test/timing.test.js:
--------------------------------------------------------------------------------
1 | const assert = require('node:assert');
2 | const fs = require('node:fs/promises');
3 | const path = require('node:path');
4 | const mm = require('egg-mock');
5 | const { sleep } = require('./utils');
6 |
7 | describe('test/timing.test.js', () => {
8 | const timingJSON = path.join(__dirname, 'fixtures/timing/run/agent_timing_11111.json');
9 | const configJSON = path.join(__dirname, 'fixtures/timing/run/application_config.json');
10 | let app;
11 | before(async () => {
12 | await fs.mkdir(path.dirname(timingJSON), { recursive: true });
13 | await fs.writeFile(timingJSON, '[]');
14 | await fs.writeFile(configJSON, '{}');
15 | mm.env('local');
16 | app = mm.cluster({
17 | baseDir: 'timing',
18 | });
19 | await app.ready();
20 | });
21 | after(() => app.close());
22 |
23 | it('should clean all timing json in agent', async () => {
24 | await app.httpRequest()
25 | .get('/checkFile')
26 | .expect({
27 | timing: false,
28 | config: true,
29 | });
30 | });
31 |
32 | it('should render page', async () => {
33 | await sleep(1000);
34 |
35 | const res = await app.httpRequest()
36 | .get('/__loader_trace__');
37 |
38 | let json = res.text.match(/data = (.*?);/);
39 | json = JSON.parse(json[1]);
40 | assert.equal(json.length, 114);
41 |
42 | const first = json[0];
43 | assert(first.type === 'agent');
44 | assert(typeof first.pid === 'string');
45 | assert.deepEqual(first.range, [ first.start, first.end ]);
46 | assert(first.title === 'agent(0)');
47 |
48 | const last = json[json.length - 1];
49 | // console.log(last);
50 | assert.match(last.type, /^app_\d+$/);
51 | assert.equal(typeof last.pid, 'string');
52 | assert.deepEqual(last.range, [ last.start, last.end ]);
53 | assert.match(last.title, /^app_\d+\(67\)$/);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/development.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 | const assert = require('node:assert');
4 | const mm = require('egg-mock');
5 | const { escape, sleep } = require('./utils');
6 |
7 | describe('test/development.test.js', () => {
8 | let app;
9 | before(() => {
10 | mm.env('local');
11 | app = mm.cluster({
12 | baseDir: 'development',
13 | });
14 | return app.ready();
15 | });
16 | after(() => app.close());
17 | afterEach(mm.restore);
18 | // for debounce
19 | afterEach(() => sleep(500));
20 |
21 | it('should reload when change service', async () => {
22 | const filepath = path.join(__dirname, 'fixtures/development/app/service/a.js');
23 | await fs.writeFile(filepath, '');
24 | await sleep(5000);
25 |
26 | await fs.unlink(filepath);
27 | app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
28 | });
29 |
30 | it('should not reload when change assets', async () => {
31 | const filepath = path.join(__dirname, 'fixtures/development/app/assets/b.js');
32 | await fs.writeFile(filepath, '');
33 | await sleep(5000);
34 |
35 | await fs.unlink(filepath);
36 | app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
37 | });
38 |
39 | it('should reload once when 2 file change', async () => {
40 | const filepath = path.join(__dirname, 'fixtures/development/app/service/c.js');
41 | const filepath1 = path.join(__dirname, 'fixtures/development/app/service/d.js');
42 | await fs.writeFile(filepath, '');
43 | // set a timeout for watcher's interval
44 | await sleep(1000);
45 | await fs.writeFile(filepath1, '');
46 |
47 | await sleep(2000);
48 | await fs.unlink(filepath);
49 | await fs.unlink(filepath1);
50 |
51 | assert(count(app.stdout, 'reload worker'), 2);
52 | });
53 | });
54 |
55 | function count(str, match) {
56 | return str.match(new RegExp(match, 'g'));
57 | }
58 |
--------------------------------------------------------------------------------
/test/override.test.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs/promises');
2 | const path = require('node:path');
3 | const mm = require('egg-mock');
4 | const { escape, sleep } = require('./utils');
5 |
6 | describe('test/override.test.js', () => {
7 | let app;
8 |
9 | after(() => app && app.close());
10 | afterEach(mm.restore);
11 | // for debounce
12 | afterEach(() => sleep(500));
13 |
14 | describe('overrideDefault', () => {
15 | before(() => {
16 | mm.env('local');
17 | app = mm.cluster({
18 | baseDir: 'override',
19 | });
20 | app.debug();
21 | return app.ready();
22 | });
23 | it('should reload', async () => {
24 | const filepath = path.join(__dirname, 'fixtures/override/app/service/a.js');
25 | await fs.writeFile(filepath, '');
26 | await sleep(1000);
27 |
28 | await fs.unlink(filepath);
29 | app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
30 | });
31 |
32 | it('should not reload', async () => {
33 | app.debug();
34 | const filepath = path.join(__dirname, 'fixtures/override/app/no-trigger/index.js');
35 | await fs.writeFile(filepath, '');
36 | await sleep(1000);
37 |
38 | await fs.unlink(filepath);
39 | app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`)));
40 | });
41 | });
42 |
43 | describe('overrideIgnore', () => {
44 | before(() => {
45 | mm.env('local');
46 | app = mm.cluster({
47 | baseDir: 'override-ignore',
48 | });
49 | app.debug();
50 | return app.ready();
51 | });
52 | it('should reload', async () => {
53 | const filepath = path.join(__dirname, 'fixtures/override-ignore/app/web/a.js');
54 | await fs.writeFile(filepath, '');
55 | await sleep(1000);
56 |
57 | await fs.unlink(filepath);
58 | app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`)));
59 | });
60 |
61 | it('should not reload', async () => {
62 | app.debug();
63 | const filepath = path.join(__dirname, 'fixtures/override-ignore/app/public/index.js');
64 | await fs.writeFile(filepath, '');
65 | await sleep(1000);
66 |
67 | await fs.unlink(filepath);
68 | app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`)));
69 | });
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/agent.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const fs = require('node:fs/promises');
3 | const debounce = require('debounce');
4 | const multimatch = require('multimatch');
5 | const { exists } = require('utility');
6 |
7 | module.exports = agent => {
8 | // clean all timing json
9 | agent.beforeStart(async () => {
10 | const rundir = agent.config.rundir;
11 | const stat = await exists(rundir);
12 | if (!stat) return;
13 | const files = await fs.readdir(rundir);
14 | for (const file of files) {
15 | if (!/^(agent|application)_timing/.test(file)) continue;
16 | await fs.rm(path.join(agent.config.rundir, file), { force: true, recursive: true });
17 | }
18 | });
19 |
20 | // single process mode don't watch and reload
21 | if (agent.options && agent.options.mode === 'single') return;
22 |
23 | const logger = agent.logger;
24 | const baseDir = agent.config.baseDir;
25 | const config = agent.config.development;
26 |
27 | let watchDirs = config.overrideDefault ? [] : [
28 | 'app',
29 | 'config',
30 | 'mocks',
31 | 'mocks_proxy',
32 | 'app.js',
33 | ];
34 |
35 | watchDirs = watchDirs.concat(config.watchDirs).map(dir => path.resolve(baseDir, dir));
36 |
37 | let ignoreReloadFileDirs = config.overrideIgnore ? [] : [
38 | 'app/views',
39 | 'app/view',
40 | 'app/assets',
41 | 'app/public',
42 | 'app/web',
43 | ];
44 |
45 | ignoreReloadFileDirs = ignoreReloadFileDirs.concat(config.ignoreDirs).map(dir => path.resolve(baseDir, dir));
46 |
47 | const reloadFile = debounce(function(info) {
48 | logger.warn(`[agent:development] reload worker because ${info.path} ${info.event}`);
49 |
50 | process.send({
51 | to: 'master',
52 | action: 'reload-worker',
53 | });
54 | }, 200);
55 |
56 |
57 | // watch dirs to reload worker, will debounce 200ms
58 | agent.watcher.watch(watchDirs, reloadWorker);
59 |
60 | /**
61 | * reload app worker:
62 | * [AgentWorker] - on file change
63 | * |-> emit reload-worker
64 | * [Master] - receive reload-worker event
65 | * |-> TODO: Mark worker will die
66 | * |-> Fork new worker
67 | * |-> kill old worker
68 | *
69 | * @param {Object} info - changed fileInfo
70 | */
71 | function reloadWorker(info) {
72 | if (!config.reloadOnDebug) {
73 | return;
74 | }
75 |
76 | if (isAssetsDir(info.path) || info.isDirectory) {
77 | return;
78 | }
79 |
80 | // don't reload if don't match
81 | if (config.reloadPattern && multimatch(info.path, config.reloadPattern).length === 0) {
82 | return;
83 | }
84 |
85 | reloadFile(info);
86 | }
87 |
88 | function isAssetsDir(path) {
89 | for (const ignorePath of ignoreReloadFileDirs) {
90 | if (path.startsWith(ignorePath)) {
91 | return true;
92 | }
93 | }
94 | return false;
95 | }
96 | };
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # egg-development
2 |
3 | [![NPM version][npm-image]][npm-url]
4 | [](https://github.com/eggjs/development/actions/workflows/nodejs.yml)
5 | [![Test coverage][codecov-image]][codecov-url]
6 | [![Known Vulnerabilities][snyk-image]][snyk-url]
7 | [![npm download][download-image]][download-url]
8 | [](https://nodejs.org/en/download/)
9 |
10 | [npm-image]: https://img.shields.io/npm/v/egg-development.svg?style=flat-square
11 | [npm-url]: https://npmjs.org/package/egg-development
12 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/egg-development.svg?style=flat-square
13 | [codecov-url]: https://codecov.io/github/eggjs/development?branch=3.x
14 | [snyk-image]: https://snyk.io/test/npm/egg-development/badge.svg?style=flat-square
15 | [snyk-url]: https://snyk.io/test/npm/egg-development
16 | [download-image]: https://img.shields.io/npm/dm/egg-development.svg?style=flat-square
17 | [download-url]: https://npmjs.org/package/egg-development
18 |
19 | This is an egg plugin for local development, under development environment enabled by default, and closed under other environment.
20 |
21 | `egg-development` has been built-in for egg. It is enabled by default.
22 |
23 | ## Configuration
24 |
25 | see [config/config.default.js](https://github.com/eggjs/development/blob/3.x/config/config.default.js) for more detail.
26 |
27 | ## Features
28 |
29 | - Under development environment, Output request log in STDOUT, statistic and output all key parts time-consuming;
30 | - Watch file changes, and reload application;
31 |
32 | ### About Reload
33 |
34 | Under the following directory (including subdirectories) will watch file changes under development environment by default, trigger an Egg development environment server reload:
35 |
36 | - ${app_root}/app
37 | - ${app_root}/config
38 | - ${app_root}/mocks
39 | - ${app_root}/mocks_proxy
40 | - ${app_root}/app.js
41 |
42 | > set `config.development.overrideDefault` to `true` to skip defaults merge.
43 |
44 | Under the following directory (including subdirectories) will ignore file changes under development environment by default:
45 |
46 | - ${app_root}/app/view
47 | - ${app_root}/app/assets
48 | - ${app_root}/app/public
49 | - ${app_root}/app/web
50 |
51 | > set `config.development.overrideIgnore` to `true` to skip defaults merge.
52 |
53 | Developer can use `config.reloadPattern`([multimatch](https://github.com/sindresorhus/multimatch)) to control whether to reload.
54 |
55 | ```js
56 | // config/config.default.js
57 | exports.development = {
58 | // don't reload when ts fileChanged
59 | // https://github.com/sindresorhus/multimatch
60 | reloadPattern: ['**', '!**/*.ts'],
61 | };
62 | ```
63 |
64 | ### Loader Trace
65 |
66 | You can view loader trace for performance issue from `http://127.0.0.1:7001/__loader_trace__`
67 |
68 | ## Questions & Suggestions
69 |
70 | Please open an issue [here](https://github.com/eggjs/egg/issues).
71 |
72 | ## License
73 |
74 | [MIT](LICENSE)
75 |
76 | ## Contributors
77 |
78 | [](https://github.com/eggjs/development/graphs/contributors)
79 |
80 | Made with [contributors-img](https://contrib.rocks).
81 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [3.0.2](https://github.com/eggjs/egg-development/compare/v3.0.1...v3.0.2) (2024-12-22)
4 |
5 |
6 | ### Bug Fixes
7 |
8 | * check run dir exists before read it ([#33](https://github.com/eggjs/egg-development/issues/33)) ([d95db72](https://github.com/eggjs/egg-development/commit/d95db72ecb2b24c4cce33654fa533f4901dc3896))
9 |
10 | ## [3.0.1](https://github.com/eggjs/egg-development/compare/v3.0.0...v3.0.1) (2024-12-22)
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * skip read run dir when it not exists ([#32](https://github.com/eggjs/egg-development/issues/32)) ([2d24ce3](https://github.com/eggjs/egg-development/commit/2d24ce320eaf1c36601f9703f8d2a4635b636167))
16 |
17 | ## [3.0.0](https://github.com/eggjs/egg-development/compare/v2.7.0...v3.0.0) (2024-05-08)
18 |
19 |
20 | ### ⚠ BREAKING CHANGES
21 |
22 | * drop Node.js < 14 support
23 |
24 |
25 |
27 | ## Summary by CodeRabbit
28 |
29 | - **New Features**
30 | - Introduced a new GitHub Actions workflow for Node.js releases.
31 | - **Enhancements**
32 | - Updated continuous integration workflow with improved job
33 | configurations.
34 | - Enhanced file system operations across multiple JavaScript files using
35 | Node.js built-in modules.
36 | - **Bug Fixes**
37 | - Fixed directory and file operations in test suites to use updated
38 | Node.js methods.
39 | - **Documentation**
40 | - Adjusted documentation comments and removed outdated directives.
41 | - **Refactor**
42 | - Removed the use of `'use strict';` in several JavaScript files to
43 | align with modern standards.
44 | - **Dependencies**
45 | - Updated various dependencies and Node version requirements to ensure
46 | compatibility and security.
47 |
48 |
49 | ### Features
50 |
51 | * upgrade deps ([#31](https://github.com/eggjs/egg-development/issues/31)) ([af27674](https://github.com/eggjs/egg-development/commit/af27674a60b0407179db65499cf2b4b55662b06b))
52 |
53 | ---
54 |
55 |
56 | 2.7.0 / 2020-09-01
57 | ==================
58 |
59 | **features**
60 | * [[`45a653c`](http://github.com/eggjs/egg-development/commit/45a653c32188eb82d3532ce4cc833094a228c85a)] - feat: overrideIgnore (#30) (TZ | 天猪 <>)
61 |
62 | 2.6.0 / 2020-08-19
63 | ==================
64 |
65 | **features**
66 | * [[`2750b52`](http://github.com/eggjs/egg-development/commit/2750b525bba30d88e94bf622f22cfc4389cdcda5)] - feat: ignore app/web by default (#29) (TZ | 天猪 <>)
67 |
68 | 2.5.0 / 2020-05-25
69 | ==================
70 |
71 | **features**
72 | * [[`eaa9222`](http://github.com/eggjs/egg-development/commit/eaa922279c2c3bf55ffd7f394dedd09aa7ef2480)] - feat: support absolute path (#27) (TZ | 天猪 <>)
73 |
74 | **fixes**
75 | * [[`b661cad`](http://github.com/eggjs/egg-development/commit/b661cad0251bb580c04ad2b8e7f35b20c765820b)] - fix: incorrect debounce on windows (#28) (hyj1991 <>)
76 |
77 | **others**
78 | * [[`3652692`](http://github.com/eggjs/egg-development/commit/3652692dfa929040b3e35f05a7b03ae19b2e476b)] - chore: update travis (#25) (TZ | 天猪 <>)
79 | * [[`e5b4aa8`](http://github.com/eggjs/egg-development/commit/e5b4aa88fb1db901ca550fd1db95afd94c596a63)] - docs: update doc (#24) (Miaolegemie <<1942037006@qq.com>>)
80 |
81 | 2.4.3 / 2019-05-07
82 | ==================
83 |
84 | **fixes**
85 | * [[`8c716f1`](http://github.com/eggjs/egg-development/commit/8c716f1bca5a44478229abf5ff2c47bc6fb3822f)] - fix: missing lib folder in npm package (#23) (Khaidi Chu <>)
86 |
87 | 2.4.2 / 2019-02-04
88 | ==================
89 |
90 | **fixes**
91 | * [[`1ea1c61`](http://github.com/eggjs/egg-development/commit/1ea1c618616e165da771c1bb4ff87f2abd0635b8)] - fix: don't reload in single process mode (#22) (Yiyu He <>)
92 |
93 | 2.4.1 / 2018-08-02
94 | ==================
95 |
96 | **fixes**
97 | * [[`88f2967`](http://github.com/eggjs/egg-development/commit/88f2967207a43ba6a7e79a3426d3d1eae78fa292)] - fix: load router by middleware (#20) (Yiyu He <>)
98 |
99 | 2.4.0 / 2018-07-31
100 | ==================
101 |
102 | * feat: app to watch list (#19)
103 |
104 | 2.3.1 / 2018-06-20
105 | ==================
106 |
107 | **others**
108 | * [[`f87bee2`](http://github.com/eggjs/egg-development/commit/f87bee2171f29b042e88503896c8e8de11be6167)] - chore: missing lib directory in pkg.files (#18) (Haoliang Gao <>)
109 |
110 | 2.3.0 / 2018-06-20
111 | ==================
112 |
113 | **features**
114 | * [[`3b6c138`](http://github.com/eggjs/egg-development/commit/3b6c1387712b68f7e0eb9833f7c486e37d77f8fd)] - feat: pick show loader timing (#17) (Haoliang Gao <>)
115 |
116 | 2.2.0 / 2018-02-06
117 | ==================
118 |
119 | **features**
120 | * [[`307dc9c`](http://github.com/eggjs/egg-development/commit/307dc9c2659adea5fa6e9bcb65e8db178f8de366)] - feat: support custom `config.development.reloadPattern ` (#14) (TZ | 天猪 <>)
121 |
122 | 2.1.0 / 2017-12-13
123 | ==================
124 |
125 | **features**
126 | * [[`252929f`](http://github.com/eggjs/egg-development/commit/252929f980d055e1cec05811981e204d0d81cb23)] - feat: support override watchDirs (#13) (TZ | 天猪 <>)
127 |
128 | 2.0.0 / 2017-11-09
129 | ==================
130 |
131 | **others**
132 | * [[`4416724`](http://github.com/eggjs/egg-development/commit/44167241fdb7cd11a5c68b4de5e728aa8992dcf8)] - refactor: drop <8.x, support egg2. [BREAKING CHANGE] (#12) (TZ | 天猪 <>)
133 |
134 | 1.3.2 / 2017-11-01
135 | ==================
136 |
137 | * fix: fastReady default to false (#11)
138 |
139 | 1.3.1 / 2017-06-04
140 | ==================
141 |
142 | * docs: fix License url (#10)
143 | * chore: remove .vscode (#9)
144 |
145 | 1.3.0 / 2017-04-18
146 | ==================
147 |
148 | * feat: allow reload on debug (#8)
149 | * docs: adjust badge link (#6)
150 |
151 | 1.2.0 / 2017-01-24
152 | ==================
153 |
154 | * feat: remove log runtime (#5)
155 |
156 | 1.1.0 / 2017-01-03
157 | ==================
158 |
159 | * feat: can disable fast ready (#4)
160 |
161 | 1.0.2 / 2016-09-20
162 | ==================
163 |
164 | * refactor: !isFile -> isDirectory (#3)
165 |
166 | 1.0.1 / 2016-09-20
167 | ==================
168 |
169 | * fix: reload at remove file (#2)
170 |
171 | 1.0.0 / 2016-09-13
172 | ==================
173 | * feat: release 1.0.0
174 |
175 | 0.1.0 / 2016-09-13
176 | ==================
177 | * feat: debounce reload && docs (#1)
178 |
--------------------------------------------------------------------------------