From 086a7da6e4b680b643dacf894b7c02d068c37d38 Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sat, 21 Oct 2023 11:01:00 +0100 Subject: [PATCH] feat(serve): add --watch flag --- package-lock.json | 247 ++++++++++++++++++++- package.json | 3 +- src/commands/ServeCommand.ts | 46 ++++ tests/fixtures/consoles/watch-mode-logs.ts | 39 ++++ tests/fixtures/consoles/watch-mode.ts | 37 +++ tests/unit/commands/ServeCommandTest.ts | 18 ++ 6 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/consoles/watch-mode-logs.ts create mode 100644 tests/fixtures/consoles/watch-mode.ts diff --git a/package-lock.json b/package-lock.json index 05b0e55..7cfdcc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/core", - "version": "4.10.3", + "version": "4.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/core", - "version": "4.10.3", + "version": "4.11.0", "license": "MIT", "dependencies": { "pretty-repl": "^3.1.2", @@ -35,6 +35,7 @@ "eslint-plugin-promise": "^6.1.1", "husky": "^3.1.0", "lint-staged": "^12.5.0", + "nodemon": "^3.0.1", "prettier": "^2.8.7" }, "engines": { @@ -1795,6 +1796,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1981,6 +1988,19 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/api-contract-validator": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/api-contract-validator/-/api-contract-validator-2.2.8.tgz", @@ -2408,6 +2428,15 @@ } ] }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -2860,6 +2889,45 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -5429,6 +5497,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6253,6 +6335,12 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6649,6 +6737,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -8436,6 +8536,86 @@ } } }, + "node_modules/nodemon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", + "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/noms": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", @@ -8446,6 +8626,21 @@ "readable-stream": "~1.0.31" } }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -9387,6 +9582,12 @@ "node": ">= 0.10" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9486,6 +9687,18 @@ "string_decoder": "~0.10.x" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -10063,6 +10276,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sinon": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", @@ -10715,6 +10940,18 @@ "node": ">=12" } }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10962,6 +11199,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 6ec210c..38304e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/core", - "version": "4.10.3", + "version": "4.11.0", "description": "The plug and play Node.js framework.", "license": "MIT", "author": "João Lenon ", @@ -98,6 +98,7 @@ "eslint-plugin-promise": "^6.1.1", "husky": "^3.1.0", "lint-staged": "^12.5.0", + "nodemon": "^3.0.1", "prettier": "^2.8.7" }, "c8": { diff --git a/src/commands/ServeCommand.ts b/src/commands/ServeCommand.ts index af8c309..1f9ef37 100644 --- a/src/commands/ServeCommand.ts +++ b/src/commands/ServeCommand.ts @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +import { Log } from '@athenna/logger' +import { Config } from '@athenna/config' import { Module } from '@athenna/common' import { BaseCommand, Option } from '@athenna/artisan' @@ -18,6 +20,13 @@ export class ServeCommand extends BaseCommand { }) public env: string + @Option({ + signature: '-w, --watch', + description: 'Use nodemon to watch the application and restart on changes.', + default: false + }) + public watch: boolean + public static signature(): string { return 'serve' } @@ -37,6 +46,43 @@ export class ServeCommand extends BaseCommand { Path.bootstrap(`main.${Path.ext()}`) ) + if (this.watch) { + const nodemon = this.getNodemon() + + nodemon({ + script: entrypoint.replace('.ts', '.js') + }) + + if (Config.is('rc.bootLogs', false)) { + return + } + + let isFirstRestart = true + + nodemon + .on('start', () => { + if (isFirstRestart) { + return + } + + console.clear() + Log.channelOrVanilla('application').success( + 'Application successfully restarted' + ) + }) + .on('restart', () => { + isFirstRestart = false + }) + + return + } + await Module.resolve(entrypoint, Config.get('rc.parentURL')) } + + public getNodemon() { + const require = Module.createRequire(import.meta.url) + + return require('nodemon') + } } diff --git a/tests/fixtures/consoles/watch-mode-logs.ts b/tests/fixtures/consoles/watch-mode-logs.ts new file mode 100644 index 0000000..3861cf0 --- /dev/null +++ b/tests/fixtures/consoles/watch-mode-logs.ts @@ -0,0 +1,39 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Mock } from '@athenna/test' +import { ViewProvider } from '@athenna/view' +import { Rc, Config } from '@athenna/config' +import { ServeCommand } from '#src/commands/ServeCommand' +import { Artisan, ConsoleKernel, ArtisanProvider } from '@athenna/artisan' + +new ViewProvider().register() +new ArtisanProvider().register() + +await Config.loadAll(Path.fixtures('config')) + +await Rc.setFile(Path.pwd('package.json')) + +Path.mergeDirs(Config.get('rc.directories', {})) + +await new ConsoleKernel().registerCommands() + +const fn = function () { + return Mock.fake() +} + +fn.on = function () { + return this +} + +Config.set('rc.bootLogs', true) + +Mock.when(ServeCommand.prototype, 'getNodemon').return(fn) + +await Artisan.parse(process.argv) diff --git a/tests/fixtures/consoles/watch-mode.ts b/tests/fixtures/consoles/watch-mode.ts new file mode 100644 index 0000000..fc93222 --- /dev/null +++ b/tests/fixtures/consoles/watch-mode.ts @@ -0,0 +1,37 @@ +/** + * @athenna/core + * + * (c) João Lenon + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Mock } from '@athenna/test' +import { ViewProvider } from '@athenna/view' +import { Rc, Config } from '@athenna/config' +import { ServeCommand } from '#src/commands/ServeCommand' +import { Artisan, ConsoleKernel, ArtisanProvider } from '@athenna/artisan' + +new ViewProvider().register() +new ArtisanProvider().register() + +await Config.loadAll(Path.fixtures('config')) + +await Rc.setFile(Path.pwd('package.json')) + +Path.mergeDirs(Config.get('rc.directories', {})) + +await new ConsoleKernel().registerCommands() + +const fn = function () { + return Mock.fake() +} + +fn.on = function () { + return this +} + +Mock.when(ServeCommand.prototype, 'getNodemon').return(fn) + +await Artisan.parse(process.argv) diff --git a/tests/unit/commands/ServeCommandTest.ts b/tests/unit/commands/ServeCommandTest.ts index add0fca..0c13957 100644 --- a/tests/unit/commands/ServeCommandTest.ts +++ b/tests/unit/commands/ServeCommandTest.ts @@ -57,4 +57,22 @@ export default class ServeCommandTest extends BaseCommandTest { output.assertSucceeded() output.assertLogged('Hello from #tests/fixtures/entrypoints/main!') } + + @Test() + public async shouldBeAbleToExecuteServeCommandInWatchMode({ command }: Context) { + const output = await command.run('serve --watch', { + path: Path.fixtures('consoles/watch-mode.ts') + }) + + output.assertSucceeded() + } + + @Test() + public async shouldBeAbleToExecuteServeCommandInWatchModeWithLogs({ command }: Context) { + const output = await command.run('serve --watch', { + path: Path.fixtures('consoles/watch-mode-logs.ts') + }) + + output.assertSucceeded() + } }