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/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c8630f5..9eafb37 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -11,7 +11,7 @@ jobs: name: Node.js uses: node-modules/github-actions/.github/workflows/node-test.yml@master with: - os: 'ubuntu-latest, macos-latest' - version: '14, 16, 18, 20, 22' + os: 'macos-latest' + version: '18.19.0, 20, 22' secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 0fe7e40..c010914 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ node_modules/ coverage/ test/fixtures/**/run .DS_Store +.tshy* +.eslintcache +dist +package-lock.json +.package-lock.json diff --git a/README.md b/README.md index aa2cb2e..0aad7b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# egg-development +# @eggjs/development [![NPM version][npm-image]][npm-url] [![Node.js CI](https://github.com/eggjs/egg-development/actions/workflows/nodejs.yml/badge.svg)](https://github.com/eggjs/egg-development/actions/workflows/nodejs.yml) @@ -6,6 +6,7 @@ [![Known Vulnerabilities][snyk-image]][snyk-url] [![npm download][download-image]][download-url] [![Node.js Version](https://img.shields.io/node/v/egg-development.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/egg-development.svg?style=flat-square [npm-url]: https://npmjs.org/package/egg-development @@ -18,11 +19,15 @@ This is an egg plugin for local development, under development environment enabled by default, and closed under other environment. -`egg-development` has been built-in for egg. It is enabled by default. +`@eggjs/development` has been built-in for egg. It is enabled by default. + +## Requirements + +- egg >= 4.x ## Configuration -see [config/config.default.js](https://github.com/eggjs/egg-development/blob/master/config/config.default.js) for more detail. +see [config/config.default.ts](https://github.com/eggjs/egg-development/blob/master/src/config/config.default.ts) for more detail. ## Features @@ -52,12 +57,14 @@ Under the following directory (including subdirectories) will ignore file change Developer can use `config.reloadPattern`([multimatch](https://github.com/sindresorhus/multimatch)) to control whether to reload. -```js -// config/config.default.js -exports.development = { - // don't reload when ts fileChanged - // https://github.com/sindresorhus/multimatch - reloadPattern: ['**', '!**/*.ts'], +```ts +// config/config.default.ts +export default = { + development: { + // don't reload when css fileChanged + // https://github.com/sindresorhus/multimatch + reloadPattern: ['**', '!**/*.css'], + }, }; ``` @@ -75,6 +82,6 @@ Please open an issue [here](https://github.com/eggjs/egg/issues). ## Contributors -[![Contributors](https://contrib.rocks/image?repo=eggjs/egg-development)](https://github.com/eggjs/egg-development/graphs/contributors) +[![Contributors](https://contrib.rocks/image?repo=eggjs/development)](https://github.com/eggjs/development/graphs/contributors) Made with [contributors-img](https://contrib.rocks). diff --git a/agent.js b/agent.js deleted file mode 100755 index 7fe6ef7..0000000 --- a/agent.js +++ /dev/null @@ -1,96 +0,0 @@ -const path = require('node:path'); -const fs = require('node:fs/promises'); -const debounce = require('debounce'); -const multimatch = require('multimatch'); -const { exists } = require('utility'); - -module.exports = agent => { - // clean all timing json - agent.beforeStart(async () => { - const rundir = agent.config.rundir; - const stat = await exists(rundir); - if (!stat) return; - const files = await fs.readdir(rundir); - for (const file of files) { - if (!/^(agent|application)_timing/.test(file)) continue; - await fs.rm(path.join(agent.config.rundir, file), { force: true, recursive: true }); - } - }); - - // single process mode don't watch and reload - if (agent.options && agent.options.mode === 'single') return; - - const logger = agent.logger; - const baseDir = agent.config.baseDir; - const config = agent.config.development; - - let watchDirs = config.overrideDefault ? [] : [ - 'app', - 'config', - 'mocks', - 'mocks_proxy', - 'app.js', - ]; - - watchDirs = watchDirs.concat(config.watchDirs).map(dir => path.resolve(baseDir, dir)); - - let ignoreReloadFileDirs = config.overrideIgnore ? [] : [ - 'app/views', - 'app/view', - 'app/assets', - 'app/public', - 'app/web', - ]; - - ignoreReloadFileDirs = ignoreReloadFileDirs.concat(config.ignoreDirs).map(dir => path.resolve(baseDir, dir)); - - const reloadFile = debounce(function(info) { - logger.warn(`[agent:development] reload worker because ${info.path} ${info.event}`); - - process.send({ - to: 'master', - action: 'reload-worker', - }); - }, 200); - - - // watch dirs to reload worker, will debounce 200ms - agent.watcher.watch(watchDirs, reloadWorker); - - /** - * reload app worker: - * [AgentWorker] - on file change - * |-> emit reload-worker - * [Master] - receive reload-worker event - * |-> TODO: Mark worker will die - * |-> Fork new worker - * |-> kill old worker - * - * @param {Object} info - changed fileInfo - */ - function reloadWorker(info) { - if (!config.reloadOnDebug) { - return; - } - - if (isAssetsDir(info.path) || info.isDirectory) { - return; - } - - // don't reload if don't match - if (config.reloadPattern && multimatch(info.path, config.reloadPattern).length === 0) { - return; - } - - reloadFile(info); - } - - function isAssetsDir(path) { - for (const ignorePath of ignoreReloadFileDirs) { - if (path.startsWith(ignorePath)) { - return true; - } - } - return false; - } -}; diff --git a/app.js b/app.js deleted file mode 100755 index 3b2ab9c..0000000 --- a/app.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = app => { - // if true, then don't need to wait at local development mode - if (app.config.development.fastReady) { - process.nextTick(() => app.ready(true)); - } - app.config.coreMiddlewares.push('eggLoaderTrace'); -}; diff --git a/package.json b/package.json index 9cb0bc3..d8a0afa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { - "name": "egg-development", + "name": "@eggjs/development", "version": "3.0.2", + "publishConfig": { + "access": "public" + }, "description": "development tool for egg", "eggPlugin": { "name": "development", @@ -9,7 +12,12 @@ ], "dependencies": [ "watcher" - ] + ], + "exports": { + "import": "./dist/esm", + "require": "./dist/commonjs", + "typescript": "./src" + } }, "keywords": [ "egg", @@ -18,42 +26,73 @@ "eggPlugin" ], "dependencies": { - "debounce": "^1.1.0", + "@eggjs/core": "^6.2.11", + "debounce": "^2.2.0", "multimatch": "^5.0.0", "utility": "^2.4.0" }, "devDependencies": { - "@types/node": "^22.10.2", - "egg": "3", - "egg-bin": "6", - "egg-mock": "5", + "@arethetypeswrong/cli": "^0.17.2", + "@eggjs/bin": "7", + "@eggjs/mock": "6", + "@eggjs/supertest": "8", + "@eggjs/tsconfig": "1", + "@types/mocha": "10", + "@types/node": "22", + "egg": "beta", "eslint": "8", - "eslint-config-egg": "12", - "supertest": "^3.4.2" + "eslint-config-egg": "14", + "rimraf": "^6.0.1", + "tshy": "3", + "tshy-after": "1", + "typescript": "5" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.19.0" }, "scripts": { - "test": "npm run lint -- --fix && npm run test-local", - "test-local": "egg-bin test --ts false", - "cov": "egg-bin cov --ts false", - "lint": "eslint .", - "ci": "npm run lint && npm run cov" + "lint": "eslint --cache src test --ext .ts", + "pretest": "npm run clean && npm run lint -- --fix", + "test": "egg-bin test", + "preci": "npm run clean && npm run lint", + "ci": "egg-bin cov", + "postci": "npm run prepublishOnly && npm run clean", + "clean": "rimraf dist", + "prepublishOnly": "tshy && tshy-after && attw --pack" }, "repository": { "type": "git", - "url": "git+https://github.com/eggjs/egg-development.git" + "url": "git+https://github.com/eggjs/development.git" }, - "files": [ - "app", - "config", - "lib", - "agent.js", - "app.js" - ], "bugs": "https://github.com/eggjs/egg/issues", - "homepage": "https://github.com/eggjs/egg-development#readme", + "homepage": "https://github.com/eggjs/development#readme", "author": "jtyjty99999", - "license": "MIT" + "license": "MIT", + "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": [ + "dist", + "src" + ], + "types": "./dist/commonjs/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/agent.ts b/src/agent.ts new file mode 100755 index 0000000..bac1f95 --- /dev/null +++ b/src/agent.ts @@ -0,0 +1,106 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; +import debounce from 'debounce'; +import multimatch from 'multimatch'; +import { exists } from 'utility'; +import type { ILifecycleBoot, EggCore } from '@eggjs/core'; +import { isTimingFile } from './utils.js'; + +export default class AgentBoot implements ILifecycleBoot { + #agent: EggCore; + + constructor(agent: EggCore) { + this.#agent = agent; + } + + async didLoad() { + // clean all timing json + const rundir = this.#agent.config.rundir; + const stat = await exists(rundir); + if (!stat) return; + const files = await fs.readdir(rundir); + for (const file of files) { + if (!isTimingFile(file)) continue; + await fs.rm(path.join(rundir, file), { force: true, recursive: true }); + } + } + + async serverDidReady() { + const agent = this.#agent; + // single process mode don't watch and reload + if (agent.options && Reflect.get(agent.options, 'mode') === 'single') { + return; + } + + const logger = agent.logger; + const baseDir = agent.config.baseDir; + const config = agent.config.development; + + let watchDirs = config.overrideDefault ? [] : [ + 'app', + 'config', + 'mocks', + 'mocks_proxy', + 'app.js', + ]; + + watchDirs = watchDirs.concat(config.watchDirs).map(dir => path.resolve(baseDir, dir)); + + let ignoreReloadFileDirs = config.overrideIgnore ? [] : [ + 'app/views', + 'app/view', + 'app/assets', + 'app/public', + 'app/web', + ]; + + ignoreReloadFileDirs = ignoreReloadFileDirs.concat(config.ignoreDirs).map(dir => path.resolve(baseDir, dir)); + + const reloadFile = debounce(function(info) { + logger.warn(`[agent:development] reload worker because ${info.path} ${info.event}`); + + process.send!({ + to: 'master', + action: 'reload-worker', + }); + }, 200); + + // watch dirs to reload worker, will debounce 200ms + /** + * reload app worker: + * [AgentWorker] - on file change + * |-> emit reload-worker + * [Master] - receive reload-worker event + * |-> TODO: Mark worker will die + * |-> Fork new worker + * |-> kill old worker + * + * @param {Object} info - changed fileInfo + */ + agent.watcher.watch(watchDirs, info => { + if (!config.reloadOnDebug) { + return; + } + + if (isAssetsDir(info.path) || info.isDirectory) { + return; + } + + // don't reload if don't match + if (config.reloadPattern && multimatch(info.path, config.reloadPattern).length === 0) { + return; + } + + reloadFile(info); + }); + + function isAssetsDir(filepath: string) { + for (const ignorePath of ignoreReloadFileDirs) { + if (filepath.startsWith(ignorePath)) { + return true; + } + } + return false; + } + } +} diff --git a/src/app.ts b/src/app.ts new file mode 100755 index 0000000..5ac59c4 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,17 @@ +import type { ILifecycleBoot, EggCore } from '@eggjs/core'; + +export default class AppBoot implements ILifecycleBoot { + #app: EggCore; + + constructor(app: EggCore) { + this.#app = app; + // if true, then don't need to wait at local development mode + if (app.config.development.fastReady) { + process.nextTick(() => this.#app.ready(true)); + } + } + + async configWillLoad() { + this.#app.config.coreMiddleware.push('eggLoaderTrace'); + } +} diff --git a/app/middleware/egg_loader_trace.js b/src/app/middleware/egg_loader_trace.ts similarity index 54% rename from app/middleware/egg_loader_trace.js rename to src/app/middleware/egg_loader_trace.ts index b49f4e5..2e74df8 100644 --- a/app/middleware/egg_loader_trace.js +++ b/src/app/middleware/egg_loader_trace.ts @@ -1,22 +1,26 @@ -const path = require('node:path'); -const fs = require('node:fs/promises'); -const { readJSON } = require('utility'); +import path from 'node:path'; +import fs from 'node:fs/promises'; +import { readJSON } from 'utility'; +import type { EggCore, MiddlewareFunc } from '@eggjs/core'; +import { getSourceFile, isTimingFile } from '../../utils.js'; -module.exports = (_, app) => { +export default (_: unknown, app: EggCore): MiddlewareFunc => { return async (ctx, next) => { - if (ctx.path !== '/__loader_trace__') return await next(); - const template = await fs.readFile(path.join(__dirname, '../../lib/loader_trace.html'), 'utf8'); + if (ctx.path !== '/__loader_trace__') { + return await next(); + } + const template = await fs.readFile(getSourceFile('config/loader_trace.html'), 'utf8'); const data = await loadTimingData(app); ctx.body = template.replace('{{placeholder}}', JSON.stringify(data)); }; }; -async function loadTimingData(app) { +async function loadTimingData(app: EggCore) { const rundir = app.config.rundir; const files = await fs.readdir(rundir); - const data = []; + const data: any[] = []; for (const file of files) { - if (!/^(agent|application)_timing/.test(file)) continue; + if (!isTimingFile(file)) continue; const json = await readJSON(path.join(rundir, file)); const isAgent = /^agent/.test(file); for (const item of json) { diff --git a/config/config.default.js b/src/config/config.default.ts similarity index 71% rename from config/config.default.js rename to src/config/config.default.ts index 4a139be..c53cc60 100755 --- a/config/config.default.js +++ b/src/config/config.default.ts @@ -1,3 +1,5 @@ +import type { DevelopmentConfig } from '../types.js'; + /** * @member Config#development * @property {Array} watchDirs - dirs needed watch, when files under these change, application will reload, use relative path @@ -8,12 +10,14 @@ * @property {Boolean} overrideIgnore - whether override default ignoreDirs, default is false. * @property {Array|String} reloadPattern - whether to reload, use https://github.com/sindresorhus/multimatch */ -exports.development = { - watchDirs: [], - ignoreDirs: [], - fastReady: false, - reloadOnDebug: true, - overrideDefault: false, - overrideIgnore: false, - reloadPattern: undefined, +export default { + development: { + watchDirs: [], + ignoreDirs: [], + fastReady: false, + reloadOnDebug: true, + overrideDefault: false, + overrideIgnore: false, + reloadPattern: undefined, + } as DevelopmentConfig, }; diff --git a/lib/loader_trace.html b/src/config/loader_trace.html similarity index 100% rename from lib/loader_trace.html rename to src/config/loader_trace.html diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ce5fb25 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +import './types.js'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5265134 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,37 @@ +export interface DevelopmentConfig { + /** + * dirs needed watch, when files under these change, application will reload, use relative path + */ + watchDirs: string[]; + /** + * dirs don't need watch, including subdirectories, use relative path + */ + ignoreDirs: string[]; + /** + * don't wait all plugins ready, default is false. + */ + fastReady: boolean; + /** + * whether reload on debug, default is true. + */ + reloadOnDebug: boolean; + /** + * whether override default watchDirs, default is false. + */ + overrideDefault: boolean; + /** + * whether override default ignoreDirs, default is false. + */ + overrideIgnore: boolean; + /** + * whether to reload, use https://github.com/sindresorhus/multimatch + */ + reloadPattern?: string[] | string; +} + +declare module '@eggjs/core' { + // add EggAppConfig overrides types + interface EggAppConfig { + development: DevelopmentConfig; + } +} diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts new file mode 100644 index 0000000..53c65c7 --- /dev/null +++ b/src/typings/index.d.ts @@ -0,0 +1,4 @@ +// make sure to import egg typings and let typescript know about it +// @see https://github.com/whxaxes/blog/issues/11 +// and https://www.typescriptlang.org/docs/handbook/declaration-merging.html +import 'egg'; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..fb2248c --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,19 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export function getSourceDirname() { + if (typeof __dirname !== 'undefined') { + return path.dirname(__dirname); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return path.dirname(fileURLToPath(import.meta.url)); +} + +export function getSourceFile(filename: string) { + return path.join(getSourceDirname(), filename); +} + +export function isTimingFile(file: string) { + return /^(agent|application)_timing/.test(file); +} diff --git a/test/absolute.test.js b/test/absolute.test.ts similarity index 52% rename from test/absolute.test.js rename to test/absolute.test.ts index fc0db11..05269b7 100755 --- a/test/absolute.test.js +++ b/test/absolute.test.ts @@ -1,38 +1,39 @@ -const fs = require('node:fs/promises'); -const path = require('node:path'); -const mm = require('egg-mock'); -const { sleep } = require('./utils'); +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { getFilepath } from './utils.js'; -describe('test/absolute.test.js', () => { - let app; +describe('test/absolute.test.ts', () => { + let app: MockApplication; before(async () => { - await fs.rm(path.join(__dirname, 'fixtures/absolute/lib'), { force: true, recursive: true }); + await fs.rm(getFilepath('absolute/lib'), { force: true, recursive: true }); // FIXME: ONLY WATCH EXIST DIR - const filepath = path.join(__dirname, 'fixtures/absolute/lib/a/b.js'); + const filepath = getFilepath('absolute/lib/a/b.js'); await fs.mkdir(path.dirname(filepath), { recursive: true }); await fs.writeFile(filepath, ''); mm.env('local'); app = mm.cluster({ baseDir: 'absolute', - debug: true, + // debug: true, }); return app.ready(); }); after(() => app.close()); afterEach(mm.restore); // for debounce - afterEach(() => sleep(500)); + afterEach(() => scheduler.wait(500)); it('should reload at absolute path', async () => { - const filepath = path.join(__dirname, 'fixtures/absolute/lib/a/b.js'); + const filepath = getFilepath('absolute/lib/a/b.js'); await fs.mkdir(path.dirname(filepath), { recursive: true }); console.log(`write file to ${filepath}`); await fs.writeFile(filepath, 'console.log(1);'); - await sleep(5000); - + await scheduler.wait(1000); await fs.unlink(filepath); + await scheduler.wait(5000); app.expect('stdout', /reload worker because .*?b\.js/); }); diff --git a/test/custom.test.js b/test/custom.test.js deleted file mode 100755 index e3e821f..0000000 --- a/test/custom.test.js +++ /dev/null @@ -1,37 +0,0 @@ -const fs = require('fs/promises'); -const path = require('node:path'); -const mm = require('egg-mock'); -const { escape, sleep } = require('./utils'); - -describe('test/custom.test.js', () => { - let app; - before(() => { - mm.env('local'); - app = mm.cluster({ - baseDir: 'custom', - }); - app.debug(); - return app.ready(); - }); - after(() => app.close()); - afterEach(mm.restore); - // for debounce - afterEach(() => sleep(500)); - - it('should reload with custom detect', async () => { - let filepath; - filepath = path.join(__dirname, 'fixtures/custom/app/service/a.js'); - await fs.writeFile(filepath, ''); - await sleep(5000); - - await fs.unlink(filepath); - app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); - - filepath = path.join(__dirname, 'fixtures/custom/app/service/b.ts'); - await fs.writeFile(filepath, ''); - await sleep(5000); - - await fs.unlink(filepath); - app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); - }); -}); diff --git a/test/custom.test.ts b/test/custom.test.ts new file mode 100755 index 0000000..2c02bfb --- /dev/null +++ b/test/custom.test.ts @@ -0,0 +1,40 @@ +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { getFilepath, DELAY } from './utils.js'; + +describe('test/custom.test.ts', () => { + let app: MockApplication; + before(() => { + mm.env('local'); + app = mm.cluster({ + baseDir: 'custom', + }); + app.debug(); + return app.ready(); + }); + after(() => app.close()); + afterEach(mm.restore); + // for debounce + afterEach(() => scheduler.wait(500)); + + it('should reload with custom detect', async () => { + if (process.env.CI) { + return; + } + let filepath; + filepath = getFilepath('custom/app/service/a.js'); + await fs.writeFile(filepath, ''); + await scheduler.wait(DELAY); + + await fs.unlink(filepath); + app.expect('stdout', /a\.js/); + + filepath = getFilepath('custom/app/service/b.ts'); + await fs.writeFile(filepath, ''); + await scheduler.wait(DELAY); + + await fs.unlink(filepath); + app.notExpect('stdout', /b\.ts/); + }); +}); diff --git a/test/development-ts.test.ts b/test/development-ts.test.ts new file mode 100755 index 0000000..efeaba0 --- /dev/null +++ b/test/development-ts.test.ts @@ -0,0 +1,61 @@ +import fs from 'node:fs/promises'; +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { escape, getFilepath, DELAY } from './utils.js'; + +describe('test/development-ts.test.ts', () => { + let app: MockApplication; + beforeEach(() => { + mm.env('local'); + app = mm.cluster({ + baseDir: 'development-ts', + }); + return app.ready(); + }); + afterEach(() => app.close()); + afterEach(mm.restore); + // for debounce + afterEach(() => scheduler.wait(500)); + + it('should reload when change service', async () => { + const filepath = getFilepath('development-ts/app/service/a.ts'); + await fs.writeFile(filepath, ''); + await scheduler.wait(1000); + await fs.unlink(filepath); + await scheduler.wait(5000); + app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); + }); + + it('should not reload when change assets', async () => { + const filepath = getFilepath('development-ts/app/assets/b.js'); + await fs.writeFile(filepath, ''); + await scheduler.wait(1000); + await fs.unlink(filepath); + await scheduler.wait(5000); + app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); + }); + + it('should reload once when 2 file change', async () => { + if (process.env.CI) { + return; + } + const filepath = getFilepath('development-ts/app/service/c.js'); + const filepath1 = getFilepath('development-ts/app/service/d.js'); + await fs.writeFile(filepath, ''); + // set a timeout for watcher's interval + await scheduler.wait(DELAY / 2); + await fs.writeFile(filepath1, ''); + + await scheduler.wait(DELAY / 2); + await fs.unlink(filepath); + await fs.unlink(filepath1); + + assert.equal(count(app.stdout, 'reload worker'), 2); + }); +}); + +function count(str: string, match: string) { + const m = str.match(new RegExp(match, 'g')); + return m ? m.length : 0; +} diff --git a/test/development.test.js b/test/development.test.js deleted file mode 100755 index 2966c93..0000000 --- a/test/development.test.js +++ /dev/null @@ -1,57 +0,0 @@ -const fs = require('node:fs/promises'); -const path = require('node:path'); -const assert = require('node:assert'); -const mm = require('egg-mock'); -const { escape, sleep } = require('./utils'); - -describe('test/development.test.js', () => { - let app; - before(() => { - mm.env('local'); - app = mm.cluster({ - baseDir: 'development', - }); - return app.ready(); - }); - after(() => app.close()); - afterEach(mm.restore); - // for debounce - afterEach(() => sleep(500)); - - it('should reload when change service', async () => { - const filepath = path.join(__dirname, 'fixtures/development/app/service/a.js'); - await fs.writeFile(filepath, ''); - await sleep(5000); - - await fs.unlink(filepath); - app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); - }); - - it('should not reload when change assets', async () => { - const filepath = path.join(__dirname, 'fixtures/development/app/assets/b.js'); - await fs.writeFile(filepath, ''); - await sleep(5000); - - await fs.unlink(filepath); - app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); - }); - - it('should reload once when 2 file change', async () => { - const filepath = path.join(__dirname, 'fixtures/development/app/service/c.js'); - const filepath1 = path.join(__dirname, 'fixtures/development/app/service/d.js'); - await fs.writeFile(filepath, ''); - // set a timeout for watcher's interval - await sleep(1000); - await fs.writeFile(filepath1, ''); - - await sleep(2000); - await fs.unlink(filepath); - await fs.unlink(filepath1); - - assert(count(app.stdout, 'reload worker'), 2); - }); -}); - -function count(str, match) { - return str.match(new RegExp(match, 'g')); -} diff --git a/test/development.test.ts b/test/development.test.ts new file mode 100755 index 0000000..023ad2d --- /dev/null +++ b/test/development.test.ts @@ -0,0 +1,61 @@ +import fs from 'node:fs/promises'; +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { escape, getFilepath, DELAY } from './utils.js'; + +describe('test/development.test.ts', () => { + let app: MockApplication; + before(() => { + mm.env('local'); + app = mm.cluster({ + baseDir: 'development', + }); + return app.ready(); + }); + after(() => app.close()); + afterEach(mm.restore); + // for debounce + afterEach(() => scheduler.wait(500)); + + it('should reload when change service', async () => { + const filepath = getFilepath('development/app/service/a.js'); + await fs.writeFile(filepath, ''); + await scheduler.wait(1000); + await fs.unlink(filepath); + await scheduler.wait(5000); + app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); + }); + + it('should not reload when change assets', async () => { + const filepath = getFilepath('development/app/assets/b.js'); + await fs.writeFile(filepath, ''); + await scheduler.wait(1000); + await fs.unlink(filepath); + await scheduler.wait(5000); + app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); + }); + + it('should reload once when 2 file change', async () => { + if (process.env.CI) { + return; + } + const filepath = getFilepath('development/app/service/c.js'); + const filepath1 = getFilepath('development/app/service/d.js'); + await fs.writeFile(filepath, ''); + // set a timeout for watcher's interval + await scheduler.wait(DELAY / 2); + await fs.writeFile(filepath1, ''); + + await scheduler.wait(DELAY / 2); + await fs.unlink(filepath); + await fs.unlink(filepath1); + + assert.equal(count(app.stdout, 'reload worker'), 4); + }); +}); + +function count(str: string, match: string) { + const m = str.match(new RegExp(match, 'g')); + return m ? m.length : 0; +} diff --git a/test/fast_ready_false.test.js b/test/fast_ready_false.test.ts similarity index 60% rename from test/fast_ready_false.test.js rename to test/fast_ready_false.test.ts index 7031808..171beee 100644 --- a/test/fast_ready_false.test.js +++ b/test/fast_ready_false.test.ts @@ -1,34 +1,34 @@ -const mm = require('egg-mock'); -const { sleep } = require('./utils'); +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; -describe('fastReady = false', () => { - let app; +describe('test/fast_ready_false.test.ts', () => { + let app: MockApplication; beforeEach(() => { mm(process.env, 'NODE_ENV', 'development'); }); afterEach(() => app.close()); afterEach(mm.restore); // for debounce - afterEach(() => sleep(500)); + afterEach(() => scheduler.wait(500)); - it('should fast ready by default', async () => { + it('should disable fast ready by default', async () => { app = mm.cluster({ baseDir: 'delay-ready', }); await app.ready(); // We need to wait for log written, because app.logger.info is async. - await sleep(100); + await scheduler.wait(100); app.expect('stdout', /Server started./); }); - it('should not fast ready if config.development.fastReady is false', async () => { + it('should set config.development.fastReady to true work', async () => { app = mm.cluster({ baseDir: 'fast-ready', }); await app.ready(); // We need to wait for log written, because app.logger.info is async. - await sleep(300); + await scheduler.wait(300); app.expect('stdout', /delayed 200ms done./); app.expect('stdout', /Server started./); diff --git a/test/fixtures/development-ts/app/assets/.gitkeep b/test/fixtures/development-ts/app/assets/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/test/fixtures/development-ts/app/public/foo.js b/test/fixtures/development-ts/app/public/foo.js new file mode 100755 index 0000000..fb1c35f --- /dev/null +++ b/test/fixtures/development-ts/app/public/foo.js @@ -0,0 +1 @@ +alert('bar'); diff --git a/test/fixtures/development-ts/app/router.ts b/test/fixtures/development-ts/app/router.ts new file mode 100755 index 0000000..235cc82 --- /dev/null +++ b/test/fixtures/development-ts/app/router.ts @@ -0,0 +1,11 @@ +import { Application } from 'egg'; + +export default (app: Application) => { + app.get('/foo.js', async ctx => { + ctx.body = 'foo.js'; + }); + + app.get('/foo', async ctx => { + ctx.body = 'foo'; + }); +}; diff --git a/test/fixtures/development-ts/app/service/.gitkeep b/test/fixtures/development-ts/app/service/.gitkeep new file mode 100755 index 0000000..e69de29 diff --git a/test/fixtures/development-ts/config/config.default.ts b/test/fixtures/development-ts/config/config.default.ts new file mode 100644 index 0000000..c791c22 --- /dev/null +++ b/test/fixtures/development-ts/config/config.default.ts @@ -0,0 +1,10 @@ +import '../../../../src/index.js'; + +import { EggAppConfig } from 'egg'; + +export default { + keys: 'foo,bar', + development: { + fastReady: false, + }, +} as EggAppConfig; diff --git a/test/fixtures/development-ts/package.json b/test/fixtures/development-ts/package.json new file mode 100755 index 0000000..35c06e7 --- /dev/null +++ b/test/fixtures/development-ts/package.json @@ -0,0 +1,4 @@ +{ + "name": "development-ts", + "type": "module" +} diff --git a/test/fixtures/development-ts/tsconfig.json b/test/fixtures/development-ts/tsconfig.json new file mode 100644 index 0000000..ff41b73 --- /dev/null +++ b/test/fixtures/development-ts/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/test/fixtures/development/app/router.js b/test/fixtures/development/app/router.js index e0cefa5..910aa86 100755 --- a/test/fixtures/development/app/router.js +++ b/test/fixtures/development/app/router.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = app => { - app.get('/foo.js', function* () { + app.get('/foo.js', async function() { this.body = 'foo.js'; }); - app.get('/foo', function* () { + app.get('/foo', async function() { this.body = 'foo'; }); }; diff --git a/test/fixtures/development/config/config.default.js b/test/fixtures/development/config/config.default.js index 97ea0f8..10f0fb0 100644 --- a/test/fixtures/development/config/config.default.js +++ b/test/fixtures/development/config/config.default.js @@ -1,3 +1 @@ -'use strict'; - exports.keys = 'foo,bar'; diff --git a/test/fixtures/timing/app.js b/test/fixtures/timing/app.js index 9c32a42..dfa06dc 100644 --- a/test/fixtures/timing/app.js +++ b/test/fixtures/timing/app.js @@ -1,11 +1,11 @@ -'use strict'; - const fs = require('fs'); const path = require('path'); module.exports = app => { - app.checkFile = { - timing: fs.existsSync(path.join(__dirname, 'run/agent_timing_11111.json')), - config: fs.existsSync(path.join(__dirname, 'run/application_config.json')), + app.checkFile = () => { + return { + timing: fs.existsSync(path.join(__dirname, 'run/agent_timing_11111.json')), + config: fs.existsSync(path.join(__dirname, 'run/application_config.json')), + }; }; }; diff --git a/test/fixtures/timing/app/router.js b/test/fixtures/timing/app/router.js index 6b10808..e4e4cea 100644 --- a/test/fixtures/timing/app/router.js +++ b/test/fixtures/timing/app/router.js @@ -1,7 +1,5 @@ -'use strict'; - module.exports = app => { app.get('/checkFile', async ctx => { - ctx.body = ctx.app.checkFile; + ctx.body = ctx.app.checkFile(); }); }; diff --git a/test/not-reload.test.js b/test/not-reload.test.ts similarity index 55% rename from test/not-reload.test.js rename to test/not-reload.test.ts index d87e55a..d36db25 100755 --- a/test/not-reload.test.js +++ b/test/not-reload.test.ts @@ -1,10 +1,10 @@ -const fs = require('node:fs/promises'); -const path = require('node:path'); -const mm = require('egg-mock'); -const { escape, sleep } = require('./utils'); +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { escape, getFilepath, DELAY } from './utils.js'; -describe('test/not-reload.test.js', () => { - let app; +describe('test/not-reload.test.ts', () => { + let app: MockApplication; before(() => { mm.env('local'); mm(process.env, 'EGG_DEBUG', true); @@ -19,12 +19,12 @@ describe('test/not-reload.test.js', () => { after(() => app.close()); afterEach(mm.restore); // for debounce - afterEach(() => sleep(500)); + afterEach(() => scheduler.wait(500)); it('should not reload', async () => { - const filepath = path.join(__dirname, 'fixtures/not-reload/app/service/a.js'); + const filepath = getFilepath('not-reload/app/service/a.js'); await fs.writeFile(filepath, ''); - await sleep(1000); + await scheduler.wait(DELAY); await fs.unlink(filepath); app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`))); diff --git a/test/override.test.js b/test/override.test.ts similarity index 56% rename from test/override.test.js rename to test/override.test.ts index 4d55ac3..160a44a 100755 --- a/test/override.test.js +++ b/test/override.test.ts @@ -1,15 +1,15 @@ -const fs = require('node:fs/promises'); -const path = require('node:path'); -const mm = require('egg-mock'); -const { escape, sleep } = require('./utils'); +import fs from 'node:fs/promises'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; +import { escape, getFilepath } from './utils.js'; -describe('test/override.test.js', () => { - let app; +describe('test/override.test.ts', () => { + let app: MockApplication; after(() => app && app.close()); afterEach(mm.restore); // for debounce - afterEach(() => sleep(500)); + afterEach(() => scheduler.wait(500)); describe('overrideDefault', () => { before(() => { @@ -21,22 +21,22 @@ describe('test/override.test.js', () => { return app.ready(); }); it('should reload', async () => { - const filepath = path.join(__dirname, 'fixtures/override/app/service/a.js'); + const filepath = getFilepath('override/app/service/a.js'); await fs.writeFile(filepath, ''); - await sleep(1000); - + await scheduler.wait(1000); await fs.unlink(filepath); - app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); + await scheduler.wait(5000); + app.expect('stdout', /a\.js/); }); it('should not reload', async () => { app.debug(); - const filepath = path.join(__dirname, 'fixtures/override/app/no-trigger/index.js'); + const filepath = getFilepath('override/app/no-trigger/index.js'); await fs.writeFile(filepath, ''); - await sleep(1000); - + await scheduler.wait(1000); await fs.unlink(filepath); - app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`))); + await scheduler.wait(5000); + app.notExpect('stdout', /index\.js/); }); }); @@ -50,21 +50,21 @@ describe('test/override.test.js', () => { return app.ready(); }); it('should reload', async () => { - const filepath = path.join(__dirname, 'fixtures/override-ignore/app/web/a.js'); + const filepath = getFilepath('override-ignore/app/web/a.js'); await fs.writeFile(filepath, ''); - await sleep(1000); - + await scheduler.wait(1000); await fs.unlink(filepath); + await scheduler.wait(5000); app.expect('stdout', new RegExp(escape(`reload worker because ${filepath}`))); }); it('should not reload', async () => { app.debug(); - const filepath = path.join(__dirname, 'fixtures/override-ignore/app/public/index.js'); + const filepath = getFilepath('override-ignore/app/public/index.js'); await fs.writeFile(filepath, ''); - await sleep(1000); - + await scheduler.wait(1000); await fs.unlink(filepath); + await scheduler.wait(5000); app.notExpect('stdout', new RegExp(escape(`reload worker because ${filepath} change`))); }); }); diff --git a/test/process_mode_single.test.js b/test/process_mode_single.test.js deleted file mode 100644 index a3844c3..0000000 --- a/test/process_mode_single.test.js +++ /dev/null @@ -1,50 +0,0 @@ -const fs = require('node:fs/promises'); -const path = require('node:path'); -const assert = require('node:assert'); -const request = require('supertest'); -const mm = require('egg-mock'); -const { sleep } = require('./utils'); - -describe('test/process_mode_single.test.js', () => { - let app; - before(async () => { - app = await require('egg').start({ - env: 'local', - baseDir: path.join(__dirname, 'fixtures/development'), - plugins: { - development: { - enable: true, - path: path.join(__dirname, '..'), - }, - }, - }); - }); - after(() => app.close()); - afterEach(mm.restore); - - it('should not reload', async () => { - let warn = false; - mm(app.agent.logger, 'warn', msg => { - if (msg.includes('reload worker')) warn = true; - }); - await request(app.callback()).get('/foo') - .expect(200) - .expect('foo'); - const filepath = path.join(__dirname, 'fixtures/development/app/service/a.js'); - await fs.writeFile(filepath, ''); - await sleep(1000); - - await request(app.callback()).get('/foo') - .expect(200) - .expect('foo'); - - await fs.unlink(filepath); - - await request(app.callback()).get('/foo') - .expect(200) - .expect('foo'); - - assert(!warn); - }); -}); - diff --git a/test/process_mode_single.test.ts b/test/process_mode_single.test.ts new file mode 100644 index 0000000..e44d791 --- /dev/null +++ b/test/process_mode_single.test.ts @@ -0,0 +1,56 @@ +import { request } from '@eggjs/supertest'; +import fs from 'node:fs/promises'; +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { mm } from '@eggjs/mock'; +import { start, Application } from 'egg'; +import { getFilepath } from './utils.js'; + +describe('test/process_mode_single.test.ts', () => { + let app: Application; + before(async () => { + app = await start({ + env: 'local', + baseDir: getFilepath('development'), + plugins: { + development: { + enable: true, + path: getFilepath('../..'), + }, + }, + } as any); + }); + after(() => app.close()); + afterEach(mm.restore); + + it('should not reload', async () => { + let warn = false; + mm(app.agent!.logger, 'warn', (msg: string) => { + if (msg.includes('reload worker')) { + warn = true; + } + }); + await request(app.callback()) + .get('/foo') + .expect(200) + .expect('foo'); + const filepath = getFilepath('development/app/service/a.js'); + await fs.writeFile(filepath, ''); + await scheduler.wait(1000); + + await request(app.callback()) + .get('/foo') + .expect(200) + .expect('foo'); + + await fs.unlink(filepath); + + await request(app.callback()) + .get('/foo') + .expect(200) + .expect('foo'); + + assert.equal(warn, false); + }); +}); + diff --git a/test/timing.test.js b/test/timing.test.js deleted file mode 100644 index a38f091..0000000 --- a/test/timing.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const assert = require('node:assert'); -const fs = require('node:fs/promises'); -const path = require('node:path'); -const mm = require('egg-mock'); -const { sleep } = require('./utils'); - -describe('test/timing.test.js', () => { - const timingJSON = path.join(__dirname, 'fixtures/timing/run/agent_timing_11111.json'); - const configJSON = path.join(__dirname, 'fixtures/timing/run/application_config.json'); - let app; - before(async () => { - await fs.mkdir(path.dirname(timingJSON), { recursive: true }); - await fs.writeFile(timingJSON, '[]'); - await fs.writeFile(configJSON, '{}'); - mm.env('local'); - app = mm.cluster({ - baseDir: 'timing', - }); - await app.ready(); - }); - after(() => app.close()); - - it('should clean all timing json in agent', async () => { - await app.httpRequest() - .get('/checkFile') - .expect({ - timing: false, - config: true, - }); - }); - - it('should render page', async () => { - await sleep(1000); - - const res = await app.httpRequest() - .get('/__loader_trace__'); - - let json = res.text.match(/data = (.*?);/); - json = JSON.parse(json[1]); - assert.equal(json.length, 114); - - const first = json[0]; - assert(first.type === 'agent'); - assert(typeof first.pid === 'string'); - assert.deepEqual(first.range, [ first.start, first.end ]); - assert(first.title === 'agent(0)'); - - const last = json[json.length - 1]; - // console.log(last); - assert.match(last.type, /^app_\d+$/); - assert.equal(typeof last.pid, 'string'); - assert.deepEqual(last.range, [ last.start, last.end ]); - assert.match(last.title, /^app_\d+\(67\)$/); - }); -}); diff --git a/test/timing.test.ts b/test/timing.test.ts new file mode 100644 index 0000000..856e7b4 --- /dev/null +++ b/test/timing.test.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'node:assert'; +import { scheduler } from 'node:timers/promises'; +import { mm, MockApplication } from '@eggjs/mock'; + +describe('test/timing.test.ts', () => { + let app: MockApplication; + before(async () => { + mm.env('local'); + app = mm.cluster({ + baseDir: 'timing', + }); + await app.ready(); + }); + after(() => app.close()); + + it('should render page', async () => { + await scheduler.wait(1000); + + const res = await app.httpRequest() + .get('/__loader_trace__'); + + const jsonString = res.text.match(/data = (.*?);/); + assert(jsonString); + assert(jsonString[1].length > 3000); + const json = JSON.parse(jsonString[1]); + + const first = json[0]; + assert(first); + assert.equal(first.type, 'agent'); + assert.equal(typeof first.pid, 'string'); + assert.deepEqual(first.range, [ first.start, first.end ]); + assert.equal(first.title, 'agent(0)'); + + const last = json[json.length - 1]; + // console.log(last); + assert.match(last.type, /^app_\d+$/); + assert.equal(typeof last.pid, 'string'); + assert.deepEqual(last.range, [ last.start, last.end ]); + assert.match(last.title, /^app_\d+\(\d+\)$/); + }); +}); diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index a9d4f2a..0000000 --- a/test/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -exports.escape = str => { - return str - .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') - .replace(/-/g, '\\x2d'); -}; - -exports.sleep = ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -}; diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..f0c26f8 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,17 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixtures = path.join(__dirname, 'fixtures'); + +export function getFilepath(name: string) { + return path.join(fixtures, name); +} + +export function escape(str: string) { + return str + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d'); +} + +export const DELAY = process.env.CI ? 30000 : 5000; 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" + } +}