├── 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 | [![CI](https://github.com/eggjs/development/actions/workflows/nodejs.yml/badge.svg?branch=2.x)](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 | [![Node.js Version](https://img.shields.io/node/v/egg-development.svg?style=flat)](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 | [![Contributors](https://contrib.rocks/image?repo=eggjs/development)](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 | --------------------------------------------------------------------------------