├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .nvmrc ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── cucumber.js ├── features ├── clean_exit.feature ├── compilation_error.feature ├── nodemon_options.feature ├── output_substitutions.feature ├── step_definitions │ ├── assertions.js │ ├── fileModifications.js │ ├── typescript.js │ └── webpack.js ├── support │ ├── outputMonitoring.js │ ├── templates.js │ ├── templates │ │ ├── entry.js │ │ ├── unrelated.js │ │ └── webpack.config.js │ ├── tmpDir.js │ ├── typescript.js │ ├── uuid.js │ └── webpackLauncher.js ├── typescript.feature └── zero_configuration.feature ├── package.json ├── pnpm-lock.yaml ├── release.config.js ├── src ├── index.js └── webpack-utils.js └── typings.d.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "targets": { 5 | "node": 4 6 | } 7 | }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - airbnb-base 3 | - prettier 4 | 5 | rules: 6 | guard-for-in: 'off' 7 | func-names: 'off' 8 | import/prefer-default-export: 'off' 9 | import/no-extraneous-dependencies: 10 | - error 11 | - devDependencies: 12 | - './features/**/*.js' 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - uses: pnpm/action-setup@v2 14 | with: 15 | version: 8 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 16.14 21 | cache: 'pnpm' 22 | 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | 26 | - name: Build 27 | run: pnpm build 28 | 29 | - name: Test 30 | run: pnpm test 31 | 32 | - name: Publish 33 | if: github.ref == 'refs/heads/master' 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | run: npx semantic-release 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | npm-debug.log 4 | .DS_Store 5 | .eslintignore 6 | .eslintrc.yml 7 | .editorconfig 8 | .babelrc 9 | .travis.yml 10 | .vscode 11 | features/ 12 | cucumber.js 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: true, 4 | trailingComma: 'es5', 5 | }; 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.8.2](https://github.com/Izhaki/nodemon-webpack-plugin/compare/v4.8.1...v4.8.2) (2023-10-10) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * bump nodemon from 2.0.19 to 3.0.1 ([33f60d5](https://github.com/Izhaki/nodemon-webpack-plugin/commit/33f60d5f92b5a8c4efa431b29613a751815476b9)) 7 | 8 | ## [4.8.1](https://github.com/Izhaki/nodemon-webpack-plugin/compare/v4.8.0...v4.8.1) (2022-07-11) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * bump nodemon from 2.0.16 to 2.0.19 ([922cd89](https://github.com/Izhaki/nodemon-webpack-plugin/commit/922cd89481559c98af2cf52d4b6bc2c3ffe55b92)) 14 | 15 | # [4.8.0](https://github.com/Izhaki/nodemon-webpack-plugin/compare/v4.7.1...v4.8.0) (2022-06-17) 16 | 17 | 18 | ### Features 19 | 20 | * bump nodemon to 2.0.16 ([ae4d162](https://github.com/Izhaki/nodemon-webpack-plugin/commit/ae4d162e5284835c98c8ecb217ef2a3b978c8a18)) 21 | 22 | ## [4.7.1](https://github.com/Izhaki/nodemon-webpack-plugin/compare/v4.7.0...v4.7.1) (2022-02-03) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * Typescript error with "noImplicitAny" ([071385c](https://github.com/Izhaki/nodemon-webpack-plugin/commit/071385c549e1ec4400d00320bf33034b46546b2b)), closes [#141](https://github.com/Izhaki/nodemon-webpack-plugin/issues/141) 28 | 29 | # [4.7.0](https://github.com/Izhaki/nodemon-webpack-plugin/compare/v4.6.0...v4.7.0) (2021-11-29) 30 | 31 | 32 | ### Features 33 | 34 | * add semantic-release ([04a8217](https://github.com/Izhaki/nodemon-webpack-plugin/commit/04a821701a2bf2987a3b351f0e55c9c11c47289b)) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nate Johnson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodemon Webpack Plugin 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | GitHub Workflow Status 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | Uses [Nodemon](https://nodemon.io/) to watch and restart your module's output file (presumably a server), but only when webpack is in watch mode (ie, `--watch`). 20 | 21 | Saves the need for installing, configuring and running Nodemon as a separate process. 22 | 23 | ## Installation 24 | 25 | ```bash 26 | npm install nodemon-webpack-plugin --save-dev 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const NodemonPlugin = require('nodemon-webpack-plugin'); // Ding 33 | 34 | module.exports = { 35 | entry: './src/server.js', 36 | output: { 37 | path: path.resolve('./dist'), 38 | filename: 'server.js', 39 | }, 40 | plugins: [ 41 | new NodemonPlugin(), // Dong 42 | ], 43 | }; 44 | ``` 45 | 46 | Then: 47 | 48 | ```shell 49 | $ webpack --watch 50 | ``` 51 | 52 | ## Modes 53 | 54 | ### Zero-config mode 55 | 56 | ```javascript 57 | new NodemonPlugin(); 58 | ``` 59 | 60 | Will watch and restart the output file. 61 | 62 | ### With config 63 | 64 | Provide a [Nodemon config object](https://github.com/remy/nodemon#config-files), like so: 65 | 66 | ```javascript 67 | new NodemonPlugin({ 68 | // If using more than one entry, you can specify 69 | // which output file will be restarted. 70 | script: './dist/server.js', 71 | 72 | // What to watch. 73 | watch: path.resolve('./dist'), 74 | 75 | // Arguments to pass to the script being watched. 76 | args: ['demo'], 77 | 78 | // Node arguments. 79 | nodeArgs: ['--debug=9222'], 80 | 81 | // Files to ignore. 82 | ignore: ['*.js.map'], 83 | 84 | // Extensions to watch. 85 | ext: 'js,njk,json', 86 | 87 | // Unlike the cli option, delay here is in milliseconds (also note that it's a string). 88 | // Here's 1 second delay: 89 | delay: '1000', 90 | 91 | // Detailed log. 92 | verbose: true, 93 | 94 | // Environment variables to pass to the script to be restarted 95 | env: { 96 | NODE_ENV: 'development', 97 | }, 98 | }); 99 | ``` 100 | 101 | For a full list of options, see Nodemon's [type definitions](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/nodemon/index.d.ts) (`Settings` interface). 102 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: ['--require-module @babel/register'], 3 | }; 4 | -------------------------------------------------------------------------------- /features/clean_exit.feature: -------------------------------------------------------------------------------- 1 | Feature: Clean exit 2 | Scenario: Ctrl-C is pressed 3 | Given I run webpack in watch mode 4 | And the server has started 5 | And Ctrl-c has been pressed 6 | When the entry file is modified 7 | Then the server shouldn't restart 8 | -------------------------------------------------------------------------------- /features/compilation_error.feature: -------------------------------------------------------------------------------- 1 | Feature: Compilation error 2 | Scenario: 3 | Given a webpack configuration that yields an error 4 | And I run webpack in watch mode 5 | Then the output should include "[nodemon-webpack-plugin]: Compilation error." 6 | -------------------------------------------------------------------------------- /features/nodemon_options.feature: -------------------------------------------------------------------------------- 1 | Feature: Support nodemon options 2 | 3 | Scenario: nodemon is asked to watch the whole output directory 4 | Given the following nodemon config: 5 | """ 6 | { 7 | watch: outputDir, 8 | } 9 | """ 10 | And I run webpack in watch mode 11 | And the server has started 12 | When a file unrelated to the entry file is modified 13 | Then the output should include "[nodemon] restarting due to changes..." 14 | And the server should restart 15 | 16 | Scenario: pass arguments to the output script 17 | Given the following nodemon config: 18 | """ 19 | { 20 | args: [ '--port', '4096' ], 21 | } 22 | """ 23 | And I run webpack in watch mode 24 | Then the server should start with the arguments "[ '--port', '4096' ]" 25 | -------------------------------------------------------------------------------- /features/output_substitutions.feature: -------------------------------------------------------------------------------- 1 | Feature: Support substitutions in output filename 2 | 3 | Scenario: output filename includes substitutions 4 | Given the output filename is "[name]-[hash].js" 5 | And I run webpack in watch mode 6 | Then the server should start 7 | -------------------------------------------------------------------------------- /features/step_definitions/assertions.js: -------------------------------------------------------------------------------- 1 | import { Given, Then } from 'cucumber'; 2 | 3 | Given('the server has started', function () { 4 | return this.waitForOutputToContain('Server started'); 5 | }); 6 | 7 | Then('the output should include {string}', function (expectedOutput) { 8 | return this.waitForOutputToContain(expectedOutput); 9 | }); 10 | 11 | Then('the server should start with the arguments {string}', function (argv) { 12 | return this.waitForOutputToContain(`Server started with argv: ${argv}`); 13 | }); 14 | 15 | Then(/^the server should (?:start|restart)$/, function () { 16 | return this.waitForOutputToContain('Server started'); 17 | }); 18 | 19 | Then("the server shouldn't restart", function () { 20 | const reject = () => { 21 | throw new Error("The server restarted when it shouldn't have"); 22 | }; 23 | const fulfill = () => true; 24 | return this.waitForOutputToContain('Server started').then(reject, fulfill); // We invert the fulfill/reject logic here. 25 | }); 26 | -------------------------------------------------------------------------------- /features/step_definitions/fileModifications.js: -------------------------------------------------------------------------------- 1 | import { When } from 'cucumber'; 2 | 3 | When('the entry file is modified', function () { 4 | this.renderEntryFile(); 5 | }); 6 | 7 | When('a file unrelated to the entry file is modified', function () { 8 | this.renderUnrelatedFile(); 9 | }); 10 | -------------------------------------------------------------------------------- /features/step_definitions/typescript.js: -------------------------------------------------------------------------------- 1 | import { When, Then } from 'cucumber'; 2 | 3 | When('I compile the file using typescript', function () { 4 | this.context.webpackConfigFileName = 'webpack.config.ts'; 5 | this.context.isTypescript = true; 6 | this.tsIssues = this.runTypescript(); 7 | }); 8 | 9 | Then('there should be {int} typescript issue', function (issueCount) { 10 | if (this.tsIssues.length !== issueCount) { 11 | throw new Error( 12 | `Expected ${issueCount} issues, but got ${this.tsIssues.length}` 13 | ); 14 | } 15 | }); 16 | 17 | Then('the first issue should be {string}', function (issueMessage) { 18 | if (this.tsIssues[0] !== issueMessage) { 19 | throw new Error( 20 | `Expected ${issueMessage} issues, but got ${this.tsIssues[0]}` 21 | ); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /features/step_definitions/webpack.js: -------------------------------------------------------------------------------- 1 | import { Given } from 'cucumber'; 2 | 3 | Given('the following nodemon config:', function (nodemonConfig) { 4 | this.context.nodemonConfig = nodemonConfig; 5 | }); 6 | 7 | Given('the output filename is {string}', function (outputFileName) { 8 | this.context.outputFileName = outputFileName; 9 | }); 10 | 11 | Given('a webpack configuration that yields an error', function () { 12 | this.context.loader = 'missing-loader'; 13 | }); 14 | 15 | Given('I run webpack in watch mode', function () { 16 | this.launchWebpack(); 17 | }); 18 | 19 | Given('Ctrl-c has been pressed', function () { 20 | this.simulateCtrlC(); 21 | }); 22 | -------------------------------------------------------------------------------- /features/support/outputMonitoring.js: -------------------------------------------------------------------------------- 1 | import { Before, setDefaultTimeout } from 'cucumber'; 2 | 3 | const ONE_SECOND = 1000; 4 | const STEP_TIME_OUT = 10 * ONE_SECOND; 5 | const WAIT_TIMEOUT = STEP_TIME_OUT - ONE_SECOND; 6 | 7 | Before(() => { 8 | setDefaultTimeout(STEP_TIME_OUT); 9 | }); 10 | 11 | Before(function () { 12 | this.waitForOutputToContain = (text) => { 13 | const { output } = this; 14 | return new Promise((resolve, reject) => { 15 | /* eslint-disable no-use-before-define */ 16 | const intervalID = setInterval(checkOutput, ONE_SECOND); 17 | setTimeout(onTimeout, WAIT_TIMEOUT); 18 | /* eslint-enable no-use-before-define */ 19 | 20 | function checkOutput() { 21 | // Note that we shift the output array here. 22 | const getNextOutputBlock = () => output.shift() || ''; 23 | 24 | let isFound = false; 25 | 26 | do { 27 | isFound = getNextOutputBlock().includes(text); 28 | } while (!isFound && output.length > 0); 29 | 30 | if (isFound) { 31 | clearInterval(intervalID); 32 | resolve(); 33 | } 34 | } 35 | 36 | function onTimeout() { 37 | clearInterval(intervalID); 38 | reject(new Error('Timeout!')); 39 | } 40 | }); 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /features/support/templates.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Before } from 'cucumber'; 3 | import fs from 'fs-extra'; 4 | import Mustache from 'mustache'; 5 | import uuid from './uuid'; 6 | import webpackConfigTpl from './templates/webpack.config'; 7 | import entryTpl from './templates/entry'; 8 | import unrelatedTpl from './templates/unrelated'; 9 | 10 | const unrelatedFileName = 'dist/config.json'; 11 | let port = 3421; 12 | 13 | Before(function () { 14 | // Default context 15 | this.context = { 16 | nodemonPluginPath: path.resolve('dist'), 17 | outputFileName: 'server.js', 18 | nodemonConfig: '', 19 | loader: 'babel-loader', 20 | webpackConfigFileName: 'webpack.config.js', 21 | isTypescript: false, 22 | }; 23 | }); 24 | 25 | Before(function () { 26 | const renderTemplate = (fileName, template) => { 27 | this.context.uuid = uuid(); // uuid is used to invalidate each file checksum 28 | 29 | // Sometimes builds fail due EADDRINUSE so increase the port for each test. 30 | this.context.port = port++; // eslint-disable-line no-plusplus 31 | const outputFile = path.join(this.tmpDir, fileName); 32 | const render = Mustache.render(template, this.context); 33 | fs.outputFileSync(outputFile, render); 34 | }; 35 | 36 | this.renderWebpackConfig = () => 37 | renderTemplate(this.context.webpackConfigFileName, webpackConfigTpl); 38 | this.renderEntryFile = () => renderTemplate('server.js', entryTpl); 39 | this.renderUnrelatedFile = () => 40 | renderTemplate(unrelatedFileName, unrelatedTpl); 41 | 42 | this.renderTemplates = () => { 43 | this.renderWebpackConfig(); 44 | this.renderEntryFile(); 45 | this.renderUnrelatedFile(); 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /features/support/templates/entry.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | const path = require( 'path' ) 3 | const express = require( 'express' ) 4 | 5 | const app = express() 6 | 7 | const argv = process.argv.slice( 2 ) 8 | 9 | console.log( 'Server started with argv:', argv ) 10 | 11 | app.get( '/', ( req, res ) => { 12 | res.send( 'hello' + '{{ uuid }}' ) 13 | }) 14 | 15 | app.listen( {{ port }} ) 16 | `; 17 | -------------------------------------------------------------------------------- /features/support/templates/unrelated.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | { 3 | "domain": "www.example.com", 4 | "mongodb": { 5 | "host": "localhost", 6 | "port": 27017 7 | }, 8 | "sessionId": "{{ uuid }}" 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /features/support/templates/webpack.config.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | const path = require( 'path' ) 3 | const nodeExternals = require( 'webpack-node-externals' ) 4 | 5 | {{#isTypescript}} 6 | import NodemonPlugin from 'nodemon-webpack-plugin'; 7 | {{/isTypescript}} 8 | {{^isTypescript}} 9 | const NodemonPlugin = require( '{{{ nodemonPluginPath }}}' ) 10 | {{/isTypescript}} 11 | 12 | const baseDir = __dirname 13 | const outputDir = path.resolve( baseDir, 'dist' ) 14 | 15 | const config = { 16 | target: 'node', 17 | mode: 'development', 18 | node: { 19 | __dirname: false, 20 | __filename: false, 21 | }, 22 | 23 | entry: path.resolve( baseDir, 'server.js' ), 24 | 25 | externals: [ nodeExternals() ], 26 | 27 | output: { 28 | path: outputDir, 29 | filename: '{{{ outputFileName }}}', 30 | }, 31 | 32 | module: { 33 | rules: [{ 34 | test: /.js$/, 35 | exclude: /node_modules/, 36 | use: { 37 | loader: '{{{loader}}}', 38 | options: { 39 | presets: [ '@babel/preset-env' ], 40 | }, 41 | }, 42 | }], 43 | }, 44 | plugins: [ 45 | new NodemonPlugin({{{ nodemonConfig }}}) 46 | ], 47 | } 48 | 49 | module.exports = config 50 | `; 51 | -------------------------------------------------------------------------------- /features/support/tmpDir.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Before } from 'cucumber'; 3 | import fs from 'fs-extra'; 4 | import tmp from 'tmp'; 5 | 6 | const projectPath = path.join(__dirname, '..', '..'); 7 | 8 | const symLinkNodeModules = (fromBaseDir, toBaseDir) => { 9 | const fromNodeModulesDir = path.join(fromBaseDir, 'node_modules'); 10 | const toNodeModulesDir = path.join(toBaseDir, 'node_modules'); 11 | 12 | const moduleNames = fs.readdirSync(fromNodeModulesDir); 13 | 14 | moduleNames.forEach((moduleName) => { 15 | const fromNodeModuleDir = path.join(fromNodeModulesDir, moduleName); 16 | const toNodeModuleDir = path.join(toNodeModulesDir, moduleName); 17 | 18 | fs.createSymlinkSync(fromNodeModuleDir, toNodeModuleDir); 19 | }); 20 | }; 21 | 22 | Before(function () { 23 | const tmpObject = tmp.dirSync({ unsafeCleanup: true }); 24 | this.tmpDir = fs.realpathSync(tmpObject.name); 25 | // this.tmpDir = path.resolve( 'tmp' ) 26 | 27 | symLinkNodeModules(projectPath, this.tmpDir); 28 | }); 29 | -------------------------------------------------------------------------------- /features/support/typescript.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import path from 'path'; 3 | import { Before } from 'cucumber'; 4 | import * as ts from 'typescript'; 5 | 6 | // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API 7 | function compile(fileNames, options) { 8 | const program = ts.createProgram(fileNames, options); 9 | const allDiagnostics = ts.getPreEmitDiagnostics(program); 10 | return allDiagnostics.map((diagnostic) => diagnostic.messageText); 11 | } 12 | 13 | Before(function () { 14 | this.runTypescript = () => { 15 | this.renderWebpackConfig(); 16 | 17 | const configFilePath = path.join( 18 | this.tmpDir, 19 | this.context.webpackConfigFileName 20 | ); 21 | 22 | const baseUrl = path.join(__dirname, '../..'); 23 | 24 | return compile([configFilePath], { 25 | noEmit: true, 26 | target: ts.ScriptTarget.ES5, 27 | module: ts.ModuleKind.CommonJS, 28 | esModuleInterop: true, 29 | // We use aliases to point to the package root - that way the types 30 | // are picked 31 | baseUrl, 32 | paths: { 'nodemon-webpack-plugin': ['.'] }, 33 | }); 34 | }; 35 | }); 36 | -------------------------------------------------------------------------------- /features/support/uuid.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | import crypto from 'crypto'; 3 | 4 | // https://stackoverflow.com/a/40191779/1179377 5 | export default () => crypto.randomBytes(16).toString('hex'); 6 | -------------------------------------------------------------------------------- /features/support/webpackLauncher.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import path from 'path'; 3 | import { spawn } from 'child-process-promise'; 4 | import kill from 'tree-kill'; 5 | import { Before, After } from 'cucumber'; 6 | 7 | Before(function () { 8 | this.launchWebpack = () => { 9 | this.renderTemplates(); 10 | 11 | const webpackBin = path.relative( 12 | process.cwd(), 13 | path.join('node_modules', '.bin', 'webpack') 14 | ); 15 | const configFilePath = path.join( 16 | this.tmpDir, 17 | this.context.webpackConfigFileName 18 | ); 19 | 20 | const promise = spawn(webpackBin, ['--config', configFilePath, '--watch'], { 21 | capture: ['stderr'], 22 | }); 23 | 24 | promise 25 | .then(() => { 26 | console.log('\nWebpack stopped'); 27 | }) 28 | .catch((err) => { 29 | console.error('Error starting webpack: ', err.stderr); 30 | }); 31 | 32 | this.childProcess = promise.childProcess; 33 | 34 | this.output = []; 35 | this.childProcess.stdout.on('data', (data) => { 36 | console.log(data.toString()); 37 | this.output.push(data.toString()); 38 | }); 39 | // Note: As of https://github.com/webpack/webpack-cli/commit/6ded275ac50f80f4ea6b29bfcc676238b59322e2 40 | // All webpack output is done using the error stream. 41 | this.childProcess.stderr.on('data', (data) => { 42 | console.log(data.toString()); 43 | this.output.push(data.toString()); 44 | }); 45 | }; 46 | }); 47 | 48 | Before(function () { 49 | this.simulateCtrlC = () => { 50 | kill(this.childProcess.pid, 'SIGINT'); 51 | }; 52 | }); 53 | 54 | After(function () { 55 | if (this.childProcess) { 56 | kill(this.childProcess.pid, 'SIGINT'); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /features/typescript.feature: -------------------------------------------------------------------------------- 1 | Feature: Typescript support 2 | 3 | Scenario: A nodemon config that includes wrong setting type 4 | Given the following nodemon config: 5 | """ 6 | { 7 | noUpdateNotifier: 2, 8 | } 9 | """ 10 | When I compile the file using typescript 11 | Then there should be 1 typescript issue 12 | And the first issue should be "Type 'number' is not assignable to type 'boolean'." 13 | 14 | -------------------------------------------------------------------------------- /features/zero_configuration.feature: -------------------------------------------------------------------------------- 1 | Feature: Zero configuration 2 | Background: 3 | Given I run webpack in watch mode 4 | And the server has started 5 | 6 | Scenario: The entry file is modified 7 | When the entry file is modified 8 | Then the output should include "[nodemon] restarting due to changes..." 9 | And the server should restart 10 | 11 | Scenario: A file not related to the entry file is modified 12 | When a file unrelated to the entry file is modified 13 | Then the server shouldn't restart 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemon-webpack-plugin", 3 | "version": "4.8.2", 4 | "description": "A webpack plugin that starts and reloads a server using Nodemon.", 5 | "main": "dist/index.js", 6 | "types": "typings.d.ts", 7 | "files": [ 8 | "dist/", 9 | "typings.d.ts" 10 | ], 11 | "scripts": { 12 | "lint": "eslint .", 13 | "test": "pnpm run test:webpack4 && npm run test:webpack5", 14 | "test:webpack4": "pnpm add webpack@^4.46.0 --save-dev && cucumber-js", 15 | "test:webpack5": "pnpm add webpack@^5.73.0 --save-dev && cucumber-js", 16 | "test:only": "cucumber-js --tags @only", 17 | "build": "pnpm run lint && rimraf dist/ && babel src/ --out-dir dist/", 18 | "watch": "rimraf dist/ && babel src/ --watch --out-dir dist/", 19 | "prettier:fix": "prettier '{**/,}*.{js,jsx,ts,tsx,json,scss,css,md,html,yaml,yml}' --ignore-path=.gitignore --write", 20 | "semantic-release": "semantic-release" 21 | }, 22 | "engines": { 23 | "node": ">=8.10.0" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/Izhaki/nodemon-webpack-plugin.git" 28 | }, 29 | "keywords": [ 30 | "webpack", 31 | "nodemon", 32 | "plugin", 33 | "server", 34 | "start", 35 | "watch", 36 | "restart" 37 | ], 38 | "author": "Roey Izhaki", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/Izhaki/nodemon-webpack-plugin.git/issues" 42 | }, 43 | "homepage": "https://github.com/Izhaki/nodemon-webpack-plugin.git", 44 | "dependencies": { 45 | "@types/nodemon": "latest", 46 | "nodemon": "3.0.1" 47 | }, 48 | "peerDependencies": { 49 | "webpack": "4 || 5" 50 | }, 51 | "devDependencies": { 52 | "@babel/cli": "^7.17.10", 53 | "@babel/core": "^7.18.5", 54 | "@babel/preset-env": "^7.18", 55 | "@babel/register": "^7.17.7", 56 | "@commitlint/cli": "^15.0.0", 57 | "@commitlint/config-conventional": "^15.0.0", 58 | "@semantic-release/changelog": "^6.0.1", 59 | "@semantic-release/exec": "^6.0.2", 60 | "@semantic-release/git": "^10.0.1", 61 | "babel-loader": "8.2.2", 62 | "child-process-promise": "2.2.1", 63 | "cucumber": "5.1.0", 64 | "eslint": "8.18.0", 65 | "eslint-config-airbnb-base": "15", 66 | "eslint-config-prettier": "8", 67 | "eslint-plugin-import": "2.26.0", 68 | "express": "4.18.1", 69 | "fs-extra": "9.0.1", 70 | "husky": "^7.0.4", 71 | "lint-staged": "^10.5.3", 72 | "mustache": "4.2.0", 73 | "prettier": "^2.7.1", 74 | "rimraf": "3.0.2", 75 | "semantic-release": "^19.0.3", 76 | "tmp": "^0.2.1", 77 | "tree-kill": "^1.2.2", 78 | "typescript": "^4.7.4", 79 | "webpack": "^5.89.0", 80 | "webpack-cli": "^4.3.1", 81 | "webpack-node-externals": "2.5.2" 82 | }, 83 | "lint-staged": { 84 | "*.{js,jsx,ts,tsx,json,scss,css,md,html,yaml,yml}": [ 85 | "prettier --write", 86 | "git add" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pkgRoot: '.', 3 | branches: ['master'], 4 | plugins: [ 5 | '@semantic-release/commit-analyzer', 6 | '@semantic-release/release-notes-generator', 7 | '@semantic-release/npm', 8 | [ 9 | '@semantic-release/exec', 10 | { 11 | prepareCmd: 12 | 'npx replace-json-property package.json version ${nextRelease.version}', // eslint-disable-line no-template-curly-in-string 13 | }, 14 | ], 15 | '@semantic-release/changelog', 16 | [ 17 | '@semantic-release/git', 18 | { 19 | assets: ['package.json', 'CHANGELOG.md'], 20 | message: 21 | 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', // eslint-disable-line no-template-curly-in-string 22 | }, 23 | ], 24 | '@semantic-release/github', 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const nodemon = require('nodemon'); 4 | const { getOutputFileMeta } = require('./webpack-utils'); 5 | 6 | module.exports = class { 7 | constructor(nodemonOptions) { 8 | this.nodemonOptions = nodemonOptions; 9 | this.isWebpackWatching = false; 10 | this.isNodemonRunning = false; 11 | } 12 | 13 | apply(compiler) { 14 | const OnAfterEmit = (compilation, callback) => { 15 | if (this.isWebpackWatching) { 16 | if (compilation.errors.length > 0) { 17 | console.log('[nodemon-webpack-plugin]: Compilation error.'); 18 | } else if (!this.isNodemonRunning) { 19 | const outputFile = getOutputFileMeta( 20 | compilation, 21 | compiler.outputPath 22 | ); 23 | this.startMonitoring(outputFile); 24 | } 25 | } 26 | callback(); 27 | }; 28 | 29 | const onWatchRun = (comp, callback) => { 30 | this.isWebpackWatching = true; 31 | callback(); 32 | }; 33 | 34 | const plugin = { name: 'nodemon-webpack-plugin"' }; 35 | 36 | if (compiler.hooks) { 37 | compiler.hooks.afterEmit.tapAsync(plugin, OnAfterEmit); 38 | compiler.hooks.watchRun.tapAsync(plugin, onWatchRun); 39 | } else { 40 | compiler.plugin('after-emit', OnAfterEmit); 41 | compiler.plugin('watch-run', onWatchRun); 42 | } 43 | } 44 | 45 | startMonitoring(relativeFileName) { 46 | const nodemonOptionsDefaults = { 47 | script: relativeFileName, 48 | watch: relativeFileName, 49 | }; 50 | 51 | const nodemonOptions = { 52 | ...nodemonOptionsDefaults, 53 | ...this.nodemonOptions, 54 | }; 55 | 56 | const monitor = nodemon(nodemonOptions); 57 | 58 | monitor.on('log', ({ colour: colouredMessage }) => 59 | console.log(colouredMessage) 60 | ); 61 | 62 | this.isNodemonRunning = true; 63 | 64 | // Ensure we exit nodemon when webpack exists. 65 | process.once('exit', () => { 66 | monitor.emit('exit'); 67 | }); 68 | // Ensure Ctrl-C triggers exit. 69 | process.once('SIGINT', () => { 70 | process.exit(0); 71 | }); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/webpack-utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const notMapFile = (file) => !file.endsWith('.map'); 4 | 5 | // Returns the first assets that is not a map file 6 | const getOutputFileName = (compilation) => 7 | Object.keys(compilation.assets).filter(notMapFile)[0]; 8 | 9 | const getOutputFileMeta = (compilation, outputPath) => { 10 | const outputFileName = getOutputFileName(compilation); 11 | const absoluteFileName = path.join(outputPath, outputFileName); 12 | return path.relative('', absoluteFileName); 13 | }; 14 | 15 | module.exports = { 16 | getOutputFileMeta, 17 | }; 18 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | import { Settings as NodemonOptions } from 'nodemon'; 2 | 3 | // We prioritise supporting CommonJS than ES6 syntax here. 4 | // Typescript users will have to add `esModuleInterop` in `compilerOptions` 5 | // or set `"allowSyntheticDefaultImports": true`. 6 | // https://www.typescriptlang.org/docs/handbook/modules.html#export--and-import--require 7 | export = NodemonPlugin; 8 | 9 | declare class NodemonPlugin { 10 | constructor(options?: NodemonOptions); 11 | 12 | // yields error TS2321: Excessive stack depth comparing types 'NodemonPlugin' and '((this: Compiler, compiler: Compiler) => void) | WebpackPluginInstance'. 13 | //apply(compiler: webpack.Compiler): void; 14 | apply(compiler: any): void; 15 | } 16 | --------------------------------------------------------------------------------