├── .appveyor.yml ├── .eslintrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── node-dev ├── icons ├── node_error.png └── node_info.png ├── images ├── node-dev-linux.png └── node-dev.png ├── lib ├── cfg.js ├── clear.js ├── cli.js ├── dedupe.js ├── hook.js ├── ignore.js ├── index.js ├── ipc.js ├── loaders │ ├── get-format.mjs │ ├── ipc.mjs │ └── load.mjs ├── local-path.js ├── log.js ├── notify.js ├── resolve-main.js ├── suppress-experimental-warnings.js └── wrap.js ├── package.json └── test ├── cli.js ├── fixture ├── .node-dev.json ├── argv.js ├── builtin.mjs ├── catch-no-such-module.js ├── cluster.js ├── echo.js ├── ecma-script-module-package │ ├── .eslintrc │ ├── index.js │ ├── message.js │ └── package.json ├── ecma-script-modules.mjs ├── env.js ├── error-null.js ├── error.js ├── exit.js ├── experimental-specifier-resolution │ └── index.mjs ├── extension-options │ ├── .node-dev.json │ ├── extension.js │ └── index.js ├── gc.js ├── ignored-module.js ├── ipc-server.js ├── log.js ├── main.js ├── message.js ├── message.mjs ├── modify-extensions.js ├── no-such-module.js ├── pid.js ├── resolution.mjs ├── server.js ├── typescript-module │ ├── index.ts │ └── package.json ├── typescript │ ├── index.ts │ └── message.ts ├── uncaught-exception-handler.js └── vmtest.js ├── index.js ├── log.js ├── run.js ├── spawn ├── argv.js ├── builtin.js ├── caught.js ├── clear.js ├── cli-require.js ├── cluster.js ├── conceal.js ├── errors.js ├── esmodule.js ├── exit-code.js ├── experimental-warnings.js ├── expose-gc.js ├── extension-options.js ├── graceful-ipc.js ├── index.js ├── inspect.js ├── kill-fork.js ├── no-such-module.js ├── node-env.js ├── relay-stdin.js ├── require-extensions.js ├── restart-twice.js ├── sigterm.js ├── timestamps.js ├── typescript-module.js ├── typescript.js └── uncaught.js └── utils.js /.appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | 3 | environment: 4 | matrix: 5 | - nodejs_version: '18' 6 | - nodejs_version: '16' 7 | - nodejs_version: '14' 8 | 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | 13 | test_script: 14 | - node test | node_modules\.bin\tap-xunit | tee test-results.xml 15 | 16 | after_test: 17 | - ps: | 18 | $wc = New-Object 'System.Net.WebClient' 19 | $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\test-results.xml)) 20 | Push-AppveyorArtifact (Resolve-Path '.\test-results.xml') 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:import/errors", "plugin:import/warnings"], 7 | "plugins": ["import"], 8 | "parserOptions": { 9 | "ecmaVersion": 11 10 | }, 11 | "rules": { 12 | "eqeqeq": "error", 13 | "no-trailing-spaces": "error", 14 | "prefer-arrow-callback": "error", 15 | "semi": "error" 16 | }, 17 | "overrides": [ 18 | { 19 | "files": ["./**/*.mjs"], 20 | "parserOptions": { 21 | "sourceType": "module" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build_ubuntu: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: ["14.x", "16.x", "18.x"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm i 25 | - run: npm test 26 | 27 | build_windows: 28 | runs-on: windows-latest 29 | 30 | strategy: 31 | matrix: 32 | node-version: ["14.x", "16.x", "18.x"] 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | - run: npm i 41 | - run: npm test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintcache 4 | node_modules 5 | npm-debug.log 6 | package-lock.json 7 | test-results.xml 8 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ./node_modules/.bin/lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .appveyor.yml 2 | .eslintrc 3 | .eslintcache 4 | .github 5 | .husky 6 | .nyc_output 7 | .prettierrc 8 | .travis.yml 9 | images 10 | package-lock.json 11 | test 12 | test-results.xml 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "trailingComma": "none", 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "singleQuote": true 8 | } 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | include: 3 | - stage: lint 4 | script: npm run lint 5 | language: node_js 6 | node_js: 7 | - 18 8 | - 16 9 | - 14 10 | sudo: false 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # node-dev 2 | 3 | ## v8.0.0 / 2022-12-30 4 | 5 | - Suppress experimental warnings in node v18 (@tmont) 6 | - Drop support for node v12, new minimum version of node is v14 (@bjornstar) 7 | - [`devDependencies`] Update `@types/node`, `eslint`, `husky`, `lint-staged`, & `tap` (@bjornstar) 8 | 9 | ## v7.4.3 / 2022-04-17 10 | 11 | - [`loaders`] Pass on unsupported extension errors when format is not `builtin` or `commonjs` (@bjornstar) 12 | - [`devDependencies`] Update most devDependencies to their latest versions (@bjornstar) 13 | - [`dependencies`] Update `minimist`, `resolve` & `semver` (@bjornstar) 14 | 15 | ## v7.4.2 / 2022-03-29 16 | 17 | - [wrap] Worker threads inherit node arguments, we only need the main thread to listen for file changes (@lehni) 18 | 19 | ## v7.4.1 / 2022-03-27 20 | 21 | - [`loaders`] Do not attempt to resolve urls unless they are `file://` urls (@bjornstar) 22 | 23 | ## v7.4.0 / 2022-03-26 24 | 25 | - Use `--require` to invoke the wrapper (@kherock) 26 | - [`loaders`] Use `fileURLToPath` to ensure support on Windows (@kherock) 27 | - [`wrap`] Suppress warnings about using experimental loaders (@kherock) 28 | - [`tests`] Ensure tests pass even if warnings are emitted (@bjornstar) 29 | - [CI] Add tests for node v12.10, v12.16, and v17 (@bjornstar) 30 | 31 | ## v7.3.1 / 2022-03-24 32 | 33 | - Add `--experimental-modules` for ESM module support on node <12.17 (@bjornstar) 34 | - Use `ipc.mjs` for `get-source-loader.mjs` (@bjornstar) 35 | - [`test`] Move extensions options tests into their own directory (@bjornstar) 36 | 37 | ## v7.3.0 / 2022-03-22 38 | 39 | - Add `--no-warnings` node option (@lehni) 40 | - Enable ESM support when package type is set to `module` (@lehni) 41 | 42 | ## v7.2.0 / 2022-03-04 43 | 44 | - Add `--preserve-symlinks` node option 45 | - Update `tap` to `v15.1.6` 46 | - Update `eslint` to `v8.10.0` 47 | - [README] Fix typo 48 | - Add a more explicit test for "All command-line arguments that are not node-dev options are passed on to the node process." 49 | - [README] Add special note about delimiting scripts 50 | 51 | ## v7.1.0 / 2021-10-24 52 | 53 | - [ESM] Update `experimental-loader` to use new `load` method from node `v16.12.0` onwards 54 | 55 | ### Developer Updates 56 | 57 | - `@types/node` updated from `v14.14.37` to `v16.11.3` 58 | - `eslint` updated from `v7.25.0` to `v8.0.1` 59 | - `husky` updated from `v6.0.0` to `v7.0.4` 60 | - `lint-staged` updated from `v10.5.4` to `v11.2.3` 61 | - `ts-node` updated from `v9.1.1` to `v10.3.1` 62 | - [CI] Start testing on windows 63 | - [`test/utils`] `touchFile` can take a path 64 | - [`test/typescript`] Use `message.ts` instead of `message.js` 65 | 66 | ## v7.0.0 / 2021-05-04 67 | 68 | - [CLI] Improve command-line parsing, restore support for --require with a space 69 | - [README] Move images into repo and fix URLs 70 | - [dependencies] Update `minimist` from `v1.1.3` to `v1.2.5` 71 | - [.npmignore] Add more config files 72 | 73 | ### Developer Updates 74 | 75 | - [CI] Add github workflows 76 | - [CI] Add appveyor 77 | - [CI] Start testing against node v16 78 | - [CI] Stop testing against node v10 79 | - [`test/spawn`] Split `index` into multiple files 80 | - [`test/utils`] Replaced directory of files with a single module that contains two methods: `spawn` and `touchFile` 81 | - [`test/utils/run`] Moved `run` function directly into the `run` file 82 | - [devDependenies] Update `eslint` from `v7.23.0` to `v7.25.0` 83 | 84 | ## v6.7.0 / 2021-04-07 85 | 86 | - [New Option] `--debounce` to control how long to wait before restarting 87 | - [New Option] `--interval` to adjust the polling interval when enabled 88 | - [`test`] Stop using `tap` aliases 89 | - [`husky`] Migrate from `v4` to `v6` 90 | - [dependencies] Update `semver` from `v7.3.4` to `v7.3.5` 91 | - [devDependencies] Update `@types/node`, `eslint`, `husky`, & `tap` 92 | 93 | ## v6.6.0 / 2021-03-23 94 | 95 | - `--clear` now clears the screen on first start 96 | - `--clear` uses `\u001bc` instead of `\033[2J\033[H` 97 | - [.eslintrc] Add rules for semicolons and whitespace 98 | - [test/cli] Add tests for clear 99 | - [test/spawn] Add tests for clear 100 | - [test/spawn] Move into directory 101 | - [test/utils/spawn] Strip out control char when logging 102 | - [lib/clear] Move clear logic into separate file 103 | - [lib/index] Group similar code 104 | 105 | ## v6.5.0 / 2021-03-19 106 | 107 | - [.npmignore] We can ignore some dotfiles that aren't necessary for the module to function 108 | - [.gitignore] Add `package-lock.json` 109 | - Prefer extracting only the method names from modules that we require, this is a preparatory step for switching to import statements and enables tree shaking. 110 | - Prefer using triple equals instead of double. 111 | - Prefer using arrow functions 112 | - [lib/ignore.js] Move ignore logic into its own file 113 | - [lib/local-path.js] Move local path function into its own file 114 | - [lib/log.js] Convert to ES6 115 | - [lib/notify.js] Convert to ES6 116 | - [test] Finish converting to ES6 style code 117 | 118 | ## v6.4.0 / 2021-03-02 119 | 120 | - Update node-notifier 121 | - Remove the SIGTERM listener when a signal is received so that other listeners don't see ours. 122 | 123 | ## v6.3.1 / 2021-03-02 124 | 125 | - Remove coffeescript tests and dev dependency 126 | - Use eslint:recommended instead of airbnb-base/legacy 127 | - Add prettier 128 | - Add package-lock.json 129 | - Add lint-staged 130 | - Update the README 131 | 132 | ## v6.3.0 / 2021-02-22 133 | 134 | - Stop disconnecting from child processes, this should prevent internal EPIPE errors 135 | - Stop adding filewatchers until child processes have completed exiting 136 | - [IPC] Stop listening on `message` 137 | - [IPC] Remove extraneous `dest` arguments 138 | - [IPC] Add a connected guard on relay 139 | - [Test] Move cluster from `run` to `spawn` 140 | - [Test] Fix typo in cluster test 141 | - [Test] Cluster test now waits for children processes to successfully start up again 142 | - [Test] Add guards to IPC and cluster tests to prevent process exit from ending the test a 2nd time 143 | - [`dependency`] Update `semver` from `v7.3.2 `to `v7.3.4` 144 | - [`devDependency`] Remove `nyc` 145 | - [`devDependency`] Update `@types/node`, `eslint`, `eslint-config-airbnb-base`, `tap`, `ts-node`, & `typescript` 146 | - [`Vagrantfile`] Remove `Vagrantfile` 147 | - [`README`] Fix typo (@ivalsaraj) 148 | 149 | ## v6.2.0 / 2020-10-15 150 | 151 | - Handle multiple values of arguments in command line (Fixes #238) 152 | 153 | ## v6.1.0 / 2020-10-15 154 | 155 | - Manually wrangle node args so that we can handle `--` args coming before `-` args (Fixes #236) 156 | 157 | ## v6.0.0 / 2020-10-14 158 | 159 | - Support ESModules in node v12.11.1+ using `get-source-loader.mjs` and `resolve-loader.mjs` for earlier versions (Fixes #212) 160 | - Pass all unknown arguments to node (Fixes #198) 161 | - Add a test case for typescript using require on the command line 162 | - Add a test case for coffeescript using require on the command line 163 | - Add a test case for `--experimental-specifier-resolution=node` 164 | - Add a test case for `--inspect` 165 | - Add `ts-node/register` as a default extension (Fixes #182) 166 | - [`README.md`] Updated to explain ESModule usage, node arguments, and typescript 167 | - [`test/utils/touch-file`] Now takes the filename as an argument 168 | - [`test/utils/spawn`] Also calls the callback with stderr output 169 | 170 | ## v5.2.0 / 2020-08-19 171 | 172 | - [lib/ipc.js] Do not send unless connected 173 | 174 | ## v5.1.0 / 2020-07-28 175 | 176 | - [wrap.js] Improve uncaughtException handling to turn non-errors into errors (Fixes #231) 177 | - [ipc.js] Declare `NODE_DEV` as a variable 178 | - [ipc.js] Inline single line function only used twice 179 | - [tests] Filenames should be snake-case 180 | 181 | ## v5.0.0 / 2020-07-08 182 | 183 | - Remove `--all-deps` and `--no-deps` CLI options, use `--deps=-1` or `--deps=0` respectively 184 | - Unify `cli` and `cfg` logic to ensure CLI always overrides config files 185 | - Load order for config files now matches what is in the `README` 186 | - Add tests for notify, CLI should override config files 187 | - All config now have clear default values 188 | - Use more ES6 code 189 | - Rename `resolveMain.js` to `resolve-main.js` 190 | 191 | ## v4.3.0 / 2020-07-03 192 | 193 | - Enable `--notify` by default and add tests 194 | - Disable by passing `--notify=false` 195 | - Move cli code out of bin 196 | - Start testing cli interface 197 | - Add bin to lint 198 | 199 | ## v4.2.0 / 2020-07-03 200 | 201 | - No longer sets NODE_ENV to `development` 202 | 203 | ## v4.1.0 / 2020-07-02 204 | 205 | - Update devDependencies: 206 | - `eslint`: from `v2.0.0` to `v7.3.1` 207 | - `eslint-config-airbnb-base`: from `v3.0.1` to `v14.2.0` 208 | - `eslint-plugin-import`: from v`1.8.1` to `v2.22.0` 209 | - `tap`: from `v12.6.2` to `v14.10.7` 210 | - `touch`: from `v1.0.0` to `v3.1.0` 211 | - Removed windows restriction for `graceful_ipc` 212 | - No longer attempts to send `SIGTERM` to disconnected child processes 213 | - [package.json] Set minimum node version to 10 214 | - [package.json] Changed test script to be more cross-platform 215 | - [tests] Split tests into 3 separate files 216 | - [tests] Removed a few opportunities for race conditions to occur 217 | - [tests] Some filesystems have single second precision, so tests now wait a minimum of 1 second before touching a file 218 | 219 | ## v4.0.0 / 2019-04-22 220 | 221 | - Update dependencies: 222 | - dynamic-dedupe: from v0.2.0 to v0.3.0 223 | - node-notifier: from v4.0.2 to v5.4.0 224 | - Update devDependencies: 225 | - From coffee-script v1.8.0 to coffeescript v2.4.1 226 | - Add option 'graceful_ipc' for windows children 227 | - Read config from CWD as well as script dir 228 | - Ignore package-lock.json for git and npm 229 | - TravisCI: Test node v6 - 11, stop testing node v5 230 | - Update README for how babel is now packages 231 | - Specify minimum node version as >=6 232 | 233 | ## v3.1.3 / 2016-05-30 234 | 235 | - Update docs 236 | - Fix eslint errors 237 | - Re-enable test for #134 238 | 239 | ## v3.1.2 / 2016-05-28 240 | 241 | - Proof against weird `require.extensions`. See #134. 242 | - Ensure method patching works when filename arguments are missing. See #135. 243 | 244 | ## v3.1.1 / 2016-05-02 245 | 246 | - Enable `--notify` by default again. See #125. 247 | - Support filename option passed to VM methods. Fixes #130. 248 | 249 | ## v3.1.0 / 2016-02-22 250 | 251 | - Add `--no-notify` to disable desktop notifications. See #120. 252 | - Fix `--no-deps` option. See #119. 253 | 254 | ## v3.0.0 / 2016-01-29 255 | 256 | - Add `--respawn` to keep watching after a process exits. See #104. 257 | - Don't terminate the child process if a custom `uncaughtException` handler is registered. See #113. 258 | - Handle `-r` and `--require` node options correctly. See #111. 259 | - Add support for passing options to transpilers. See #109. 260 | - Handle `--no-deps` correctly. See #108. 261 | - Switch to airbnb code style 262 | - Use greenkeeper.io to keep dependencies up to date 263 | 264 | ## v2.7.1 / 2015-08-21 265 | 266 | - Add `--poll` to fix #87 267 | - Switch from [`commander`][npm-commander] to [`minimist`][npm-minimist] 268 | - Fix issues introduced in 2.7.0. See #102 for details. 269 | 270 | ## v2.7.0 / 2015-08-17 271 | 272 | - Support ignoring file paths, e.g. for universal (isomorphic) apps. See 273 | [`README`][readme-ignore-paths] for more details. 274 | - Use [`commander`][npm-commander] for CLI argument parsing instead of custom code. 275 | - Extract [`LICENSE`][license] file. 276 | - Upgrade [`tap`][npm-tap] module to 1.3.2. 277 | - Use [`touch`][npm-touch] module instead of custom code. 278 | 279 | [license]: LICENSE 280 | [npm-commander]: https://www.npmjs.com/package/commander 281 | [npm-minimist]: https://www.npmjs.com/package/minimist 282 | [npm-tap]: https://www.npmjs.com/package/tap 283 | [npm-touch]: https://www.npmjs.com/package/touch 284 | [readme]: README.md 285 | [readme-ignore-paths]: README.md#ignore-paths 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014–2021 Felix Gnass 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/fgnass/node-dev.png)](http://travis-ci.org/fgnass/node-dev) 2 | 3 | ### node-dev (1) 4 | 5 | Node-dev is a development tool for [Node.js](http://nodejs.org) that 6 | automatically restarts the node process when a file is modified. 7 | 8 | In contrast to tools like [supervisor](https://github.com/isaacs/node-supervisor) or [nodemon](https://github.com/remy/nodemon) it doesn't scan the filesystem for files to be watched. Instead it hooks into Node's `require()` function to watch only the files that have been _actually required_. 9 | 10 | This means that you don't have to configure any include- or exclude rules. If you modify a JS file that is solely used on the client-side but never run on the server, **node-dev will know** this and won't restart the process. 11 | 12 | This also means that you **don't have to** configure any file extensions. Just require a `.json` file or a `.ts` script for example and it will be watched. Automatically. 13 | 14 | # Usage 15 | 16 | Just run `node-dev` as you would normally run `node`: 17 | 18 | ``` 19 | node-dev server.js 20 | ``` 21 | 22 | ## TypeScript support 23 | 24 | You can use node-dev to watch and restart TypeScript projects. Install [ts-node](https://www.npmjs.com/package/ts-node) as dev-dependency, then use node-dev to run your script: 25 | 26 | ``` 27 | node-dev src/server.ts 28 | ``` 29 | 30 | ## Command-line options 31 | 32 | There are a couple of command-line options that can be used to control which files are watched and what happens when they change: 33 | 34 | - `--clear` - Clear the screen on restart 35 | - `--debounce` - Debounce change events by time in milliseconds (non-polling mode, default: 10) 36 | - `--dedupe` - [Dedupe dynamically](https://www.npmjs.org/package/dynamic-dedupe) 37 | - `--deps`: 38 | - `-1` - Watch the whole dependency tree 39 | - `0` - Watch only the project's own files and linked modules (via `npm link`) 40 | - `1` (_Default_) - Watch all first level dependencies 41 | - ` ` - Number of levels to watch 42 | - `--fork` - Hook into child_process.fork 43 | - `--graceful_ipc ` - Send 'msg' as an IPC message instead of SIGTERM for restart/shutdown 44 | - `--ignore` - A file whose changes should not cause a restart 45 | - `--interval` - Polling interval in milliseconds (default: 1000) 46 | - `--notify=false` - Disable desktop notifications 47 | - `--poll` - Force polling for file changes (Caution! CPU-heavy!) 48 | - `--respawn` - Keep watching for changes after the script has exited 49 | - `--timestamp` - The timestamp format to use for logging restarts 50 | - `--vm` - Load files using Node's VM 51 | 52 | ## Passing arguments to node 53 | 54 | All command-line arguments that are not `node-dev` options are passed on to the `node` process. 55 | 56 | Please note: you may need to separate your script from other command line options with `--`, for example: 57 | 58 | `node-dev --some-node-args -- my-script.js` 59 | 60 | # Installation 61 | 62 | `node-dev` can be installed via `npm`. Installing it with the `-g` option will allow you to use it anywhere you would use `node`. 63 | 64 | ``` 65 | npm install -g node-dev 66 | ``` 67 | 68 | ### Desktop Notifications 69 | 70 | Status and error messages can be displayed as desktop notification using 71 | [node-notifier](https://www.npmjs.org/package/node-notifier): 72 | 73 | ![Screenshot](./images/node-dev.png) 74 | 75 | ![Screenshot](./images/node-dev-linux.png) 76 | 77 | **Requirements:** 78 | 79 | - Mac OS X: >= 10.8 80 | - Linux: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default) 81 | - Windows: >= 8, or task bar balloons for Windows < 8 82 | 83 | # Config file 84 | 85 | Upon startup node-dev looks for a `.node-dev.json` file in the following directories: 86 | 87 | - the user's home directory 88 | - the current working directory 89 | - the same directory as the script to run 90 | 91 | Settings found later in the list will overwrite previous options. 92 | 93 | ## Configuration options 94 | 95 | Usually node-dev doesn't require any configuration at all, but there are some options you can set to tweak its behaviour: 96 | 97 | - `clear` – Whether to clear the screen upon restarts. _Default:_ `false` 98 | - `dedupe` – Whether modules should by [dynamically deduped](https://www.npmjs.org/package/dynamic-dedupe). _Default:_ `false` 99 | - `deps` – How many levels of dependencies should be watched. _Default:_ `1` 100 | - `fork` – Whether to hook into [child_process.fork](http://nodejs.org/docs/latest/api/child_process.html#child_process_child_process_fork_modulepath_args_options) (required for [clustered](http://nodejs.org/docs/latest/api/cluster.html) programs). _Default:_ `true` 101 | - `graceful_ipc` - Send the argument provided as an IPC message instead of SIGTERM during restart events. _Default:_ `""` (off) 102 | - `ignore` - A single file or an array of files to ignore. _Default:_ `[]` 103 | - `notify` – Whether to display desktop notifications. _Default:_ `true` 104 | - `poll` - Force polling for file changes, this can be CPU-heavy. _Default:_ `false` 105 | - `respawn` - Keep watching for changes after the script has exited. _Default:_ `false` 106 | - `timestamp` – The timestamp format to use for logging restarts. _Default:_ `"HH:MM:ss"` 107 | - `vm` – Whether to watch files loaded via Node's [VM](http://nodejs.org/docs/latest/api/vm.html) module. _Default:_ `true` 108 | 109 | ### ESModules 110 | 111 | When using ESModule syntax and `.mjs` files, `node-dev` will automatically use a loader to know which files to watch. 112 | 113 | ### Dedupe linked modules 114 | 115 | Sometimes you need to make sure that multiple modules get _exactly the same instance_ of a common (peer-) dependency. This can usually be achieved by running `npm dedupe` – however this doesn't work when you try to `npm link` a dependency (which is quite common during development). Therefore `node-dev` provides a `--dedupe` switch that will inject the [dynamic-dedupe](https://www.npmjs.org/package/dynamic-dedupe) module into your app. 116 | 117 | ### Transpilers 118 | 119 | You can use `node-dev` to run transpiled languages like TypeScript. You can either use a `.js` file as entry point to your application that registers your transpiler as a require-extension manually, for example by calling `CoffeeScript.register()` or you can let node-dev do this for you. 120 | 121 | There is a config option called `extensions` which maps file extensions to compiler module names. By default the map looks like this: 122 | 123 | ```json 124 | { 125 | "coffee": "coffee-script/register", 126 | "ls": "LiveScript", 127 | "ts": "ts-node/register" 128 | } 129 | ``` 130 | 131 | This means that if you run `node-dev server.ts` node-dev will do a 132 | `require("ts-node/register")` before running your script. You need 133 | to have `ts-node` installed as a dependency of your package. 134 | 135 | Options can be passed to a transpiler by providing an object containing `name` and `options` attributes: 136 | 137 | ```json 138 | { 139 | "js": { 140 | "name": "babel-core/register", 141 | "options": { 142 | "only": ["lib/**", "node_modules/es2015-only-module/**"] 143 | } 144 | } 145 | } 146 | ``` 147 | 148 | ### Graceful restarts 149 | 150 | Node-dev sends a `SIGTERM` signal to the child-process if a restart is required. If your app is not listening for these signals `process.exit(0)` will be called immediately. If a listener is registered, node-dev assumes that your app will exit on its own once it is ready. 151 | 152 | Windows does not handle POSIX signals, as such signals such as `SIGTERM` cause the process manager to unconditionally terminate the application with no chance of cleanup. In this case, the option `graceful_ipc` may be used. If this option is defined, the argument provided to the option will be sent as an IPC message via `child.send("")`. The child process can listen and 153 | handle this event with: 154 | 155 | ```javascript 156 | process.on('message', function (msg) { 157 | if (msg === '') { 158 | // Gracefully shut down here 159 | doGracefulShutdown(); 160 | } 161 | }); 162 | ``` 163 | 164 | ### Ignore paths 165 | 166 | If you’d like to ignore certain paths or files from triggering a restart, list them in the `.node-dev.json` configuration under `"ignore"` like this: 167 | 168 | ```json 169 | { 170 | "ignore": ["client/scripts", "shared/module.js"] 171 | } 172 | ``` 173 | 174 | This can be useful when you are running an isomorphic web app that shares modules between the server and the client. 175 | 176 | ## License 177 | 178 | MIT 179 | -------------------------------------------------------------------------------- /bin/node-dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const dev = require('../lib'); 4 | const cli = require('../lib/cli'); 5 | 6 | const { 7 | script, 8 | scriptArgs, 9 | nodeArgs, 10 | opts 11 | } = cli(process.argv); 12 | 13 | dev(script, scriptArgs, nodeArgs, opts); 14 | -------------------------------------------------------------------------------- /icons/node_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgnass/node-dev/9ab4eb61a6cc67e6bcfe9de65770e6e345179636/icons/node_error.png -------------------------------------------------------------------------------- /icons/node_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgnass/node-dev/9ab4eb61a6cc67e6bcfe9de65770e6e345179636/icons/node_info.png -------------------------------------------------------------------------------- /images/node-dev-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgnass/node-dev/9ab4eb61a6cc67e6bcfe9de65770e6e345179636/images/node-dev-linux.png -------------------------------------------------------------------------------- /images/node-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgnass/node-dev/9ab4eb61a6cc67e6bcfe9de65770e6e345179636/images/node-dev.png -------------------------------------------------------------------------------- /lib/cfg.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync } = require('fs'); 2 | const { dirname, resolve } = require('path'); 3 | 4 | const resolveMain = require('./resolve-main'); 5 | 6 | const defaultConfig = { 7 | clear: false, 8 | debounce: 10, 9 | dedupe: false, 10 | deps: 1, 11 | extensions: { 12 | coffee: 'coffeescript/register', 13 | ls: 'LiveScript', 14 | ts: 'ts-node/register' 15 | }, 16 | fork: true, 17 | graceful_ipc: '', 18 | ignore: [], 19 | interval: 1000, 20 | notify: true, 21 | poll: false, 22 | respawn: false, 23 | timestamp: 'HH:MM:ss', 24 | vm: true 25 | }; 26 | 27 | function read(dir) { 28 | const f = resolve(dir, '.node-dev.json'); 29 | return existsSync(f) ? JSON.parse(readFileSync(f)) : {}; 30 | } 31 | 32 | function getConfig(script) { 33 | const main = resolveMain(script); 34 | const dir = main ? dirname(main) : '.'; 35 | 36 | return Object.assign( 37 | defaultConfig, 38 | read(process.env.HOME || process.env.USERPROFILE), 39 | read(process.cwd()), 40 | read(dir) 41 | ); 42 | } 43 | 44 | module.exports = { 45 | defaultConfig, 46 | getConfig 47 | }; 48 | -------------------------------------------------------------------------------- /lib/clear.js: -------------------------------------------------------------------------------- 1 | const control = '\u001bc'; 2 | const clearFactory = clear => (clear ? () => process.stdout.write(control) : () => {}); 3 | 4 | module.exports = { clearFactory, control }; 5 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const minimist = require('minimist'); 3 | const { resolve } = require('path'); 4 | 5 | const { getConfig } = require('./cfg'); 6 | 7 | const arrayify = v => (Array.isArray(v) ? [...v] : [v]); 8 | const argify = key => ({ arg: `--${key}`, key }); 9 | 10 | const resolvePath = p => resolve(process.cwd(), p); 11 | 12 | const nodeAlias = { require: 'r' }; 13 | const nodeBoolean = ['expose_gc', 'preserve-symlinks']; 14 | const nodeCustom = ['inspect', 'inspect-brk', 'no-warnings']; 15 | const nodeString = ['require']; 16 | 17 | const nodeDevBoolean = ['clear', 'dedupe', 'fork', 'notify', 'poll', 'respawn', 'vm']; 18 | const nodeDevNumber = ['debounce', 'deps', 'interval']; 19 | const nodeDevString = ['graceful_ipc', 'ignore', 'timestamp']; 20 | 21 | const alias = Object.assign({}, nodeAlias); 22 | const boolean = [...nodeBoolean, ...nodeDevBoolean]; 23 | const string = [...nodeString, ...nodeDevString]; 24 | 25 | const nodeArgsReducer = 26 | opts => 27 | (out, { arg, key }) => { 28 | const value = opts[key]; 29 | 30 | if (typeof value === 'boolean') { 31 | value && out.push(arg); 32 | } else if (typeof value !== 'undefined') { 33 | arrayify(value).forEach(v => { 34 | if (arg.includes('=')) { 35 | out.push(`${arg.split('=')[0]}=${v}`); 36 | } else { 37 | out.push(`${arg}=${v}`); 38 | } 39 | }); 40 | } 41 | 42 | delete opts[key]; 43 | 44 | return out; 45 | }; 46 | 47 | const nodeCustomFactory = args => arg => { 48 | const isNodeCustom = nodeCustom.includes(arg.substring(2)); 49 | if (isNodeCustom) args.push(arg); 50 | return !isNodeCustom; 51 | }; 52 | 53 | const unknownFactory = args => arg => { 54 | const [, key] = Object.keys(minimist([arg])); 55 | key && !nodeDevNumber.includes(key) && args.push({ arg, key }); 56 | }; 57 | 58 | module.exports = argv => { 59 | const nodeCustomArgs = []; 60 | const args = argv.slice(2).filter(nodeCustomFactory(nodeCustomArgs)); 61 | 62 | const unknownArgs = []; 63 | const unknown = unknownFactory(unknownArgs); 64 | 65 | const { 66 | _: [script, ...scriptArgs] 67 | } = minimist(args, { alias, boolean, string, unknown }); 68 | 69 | assert(script, 'Could not parse command line arguments'); 70 | 71 | const opts = minimist(args, { alias, boolean, default: getConfig(script) }); 72 | const nodeArgs = [...nodeBoolean.map(argify), ...nodeString.map(argify), ...unknownArgs] 73 | .sort((a, b) => a.key - b.key) 74 | .reduce(nodeArgsReducer(opts), [...nodeCustomArgs]); 75 | 76 | opts.ignore = arrayify(opts.ignore).map(resolvePath); 77 | 78 | return { nodeArgs, opts, script, scriptArgs }; 79 | }; 80 | -------------------------------------------------------------------------------- /lib/dedupe.js: -------------------------------------------------------------------------------- 1 | require('dynamic-dedupe').activate(); 2 | -------------------------------------------------------------------------------- /lib/hook.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'); 2 | 3 | module.exports = (patchVM, callback) => { 4 | // Hook into Node's `require(...)` 5 | updateHooks(); 6 | 7 | // Patch the vm module to watch files executed via one of these methods: 8 | if (patchVM) { 9 | patch(vm, 'createScript', 1); 10 | patch(vm, 'runInThisContext', 1); 11 | patch(vm, 'runInNewContext', 2); 12 | patch(vm, 'runInContext', 2); 13 | } 14 | 15 | /** 16 | * Patch the specified method to watch the file at the given argument 17 | * index. 18 | */ 19 | function patch(obj, method, optionsArgIndex) { 20 | const orig = obj[method]; 21 | if (!orig) return; 22 | obj[method] = function () { 23 | const opts = arguments[optionsArgIndex]; 24 | let file = null; 25 | if (opts) { 26 | file = typeof opts === 'string' ? opts : opts.filename; 27 | } 28 | if (file) callback(file); 29 | return orig.apply(this, arguments); 30 | }; 31 | } 32 | 33 | /** 34 | * (Re-)install hooks for all registered file extensions. 35 | */ 36 | function updateHooks() { 37 | Object.keys(require.extensions).forEach(ext => { 38 | const fn = require.extensions[ext]; 39 | if (typeof fn === 'function' && fn.name !== 'nodeDevHook') { 40 | require.extensions[ext] = createHook(fn); 41 | } 42 | }); 43 | } 44 | 45 | /** 46 | * Returns a function that can be put into `require.extensions` in order to 47 | * invoke the callback when a module is required for the first time. 48 | */ 49 | function createHook(handler) { 50 | return function nodeDevHook(module, filename) { 51 | if (!module.loaded) callback(module.filename); 52 | 53 | // Invoke the original handler 54 | handler(module, filename); 55 | 56 | // Make sure the module did not hijack the handler 57 | updateHooks(); 58 | }; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /lib/ignore.js: -------------------------------------------------------------------------------- 1 | const n = 'node_modules'; 2 | 3 | /** 4 | * Returns the nesting-level of the given module. 5 | * Will return 0 for modules from the main package or linked modules, 6 | * a positive integer otherwise. 7 | */ 8 | const getLevel = mod => getPrefix(mod).split(n).length - 1; 9 | 10 | /** 11 | * Returns the path up to the last occurence of `node_modules` or an 12 | * empty string if the path does not contain a node_modules dir. 13 | */ 14 | const getPrefix = mod => { 15 | const i = mod.lastIndexOf(n); 16 | return i !== -1 ? mod.slice(0, i + n.length) : ''; 17 | }; 18 | 19 | const isPrefixOf = value => prefix => value.startsWith(prefix); 20 | 21 | const configureDeps = deps => required => deps !== -1 && getLevel(required) > deps; 22 | const configureIgnore = ignore => required => ignore.some(isPrefixOf(required)); 23 | 24 | module.exports = { configureDeps, configureIgnore }; 25 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { fork } = require('child_process'); 2 | const filewatcher = require('filewatcher'); 3 | const { join } = require('path'); 4 | const semver = require('semver'); 5 | const { pathToFileURL } = require('url'); 6 | 7 | const { clearFactory } = require('./clear'); 8 | const { configureDeps, configureIgnore } = require('./ignore'); 9 | const ipc = require('./ipc'); 10 | const localPath = require('./local-path'); 11 | const logFactory = require('./log'); 12 | const notifyFactory = require('./notify'); 13 | const resolveMain = require('./resolve-main'); 14 | 15 | module.exports = function ( 16 | script, 17 | scriptArgs, 18 | nodeArgs, 19 | { 20 | clear, 21 | debounce, 22 | dedupe, 23 | deps, 24 | graceful_ipc: gracefulIPC, 25 | ignore, 26 | interval, 27 | notify: notifyEnabled, 28 | poll: forcePolling, 29 | respawn, 30 | timestamp 31 | } 32 | ) { 33 | if (!script) { 34 | console.log('Usage: node-dev [options] script [arguments]\n'); 35 | process.exit(1); 36 | } 37 | 38 | if (typeof script !== 'string' || script.length === 0) { 39 | throw new TypeError('`script` must be a string'); 40 | } 41 | 42 | if (!Array.isArray(scriptArgs)) { 43 | throw new TypeError('`scriptArgs` must be an array'); 44 | } 45 | 46 | if (!Array.isArray(nodeArgs)) { 47 | throw new TypeError('`nodeArgs` must be an array'); 48 | } 49 | 50 | const clearOutput = clearFactory(clear); 51 | 52 | const log = logFactory({ timestamp }); 53 | const notify = notifyFactory(notifyEnabled, log); 54 | 55 | const isIgnored = configureIgnore(ignore); 56 | const isTooDeep = configureDeps(deps); 57 | 58 | // Run ./dedupe.js as preload script 59 | if (dedupe) process.env.NODE_DEV_PRELOAD = localPath('dedupe'); 60 | 61 | const watcher = filewatcher({ debounce, forcePolling, interval }); 62 | let isPaused = false; 63 | 64 | // The child_process 65 | let child; 66 | 67 | watcher.on('change', file => { 68 | clearOutput(); 69 | notify('Restarting', `${file} has been modified`); 70 | watcher.removeAll(); 71 | isPaused = true; 72 | if (child) { 73 | // Child is still running, restart upon exit 74 | child.on('exit', start); 75 | stop(); 76 | } else { 77 | // Child is already stopped, probably due to a previous error 78 | start(); 79 | } 80 | }); 81 | 82 | watcher.on('fallback', limit => { 83 | log.warn('node-dev ran out of file handles after watching %s files.', limit); 84 | log.warn('Falling back to polling which uses more CPU.'); 85 | log.info('Run ulimit -n 10000 to increase the file descriptor limit.'); 86 | if (deps) log.info('... or add `--deps=0` to use fewer file handles.'); 87 | }); 88 | 89 | /** 90 | * Run the wrapped script. 91 | */ 92 | function start() { 93 | isPaused = false; 94 | 95 | const args = nodeArgs.slice(); 96 | 97 | args.push(`--require=${resolveMain(localPath('wrap'))}`); 98 | 99 | const loaderName = semver.satisfies(process.version, '>=16.12.0') ? 'load' : 'get-format'; 100 | 101 | const loaderURL = pathToFileURL(resolveMain(localPath(join('loaders', `${loaderName}.mjs`)))); 102 | 103 | args.push(`--experimental-loader=${loaderURL.href}`); 104 | 105 | child = fork(script, scriptArgs, { 106 | cwd: process.cwd(), 107 | env: process.env, 108 | execArgv: args 109 | }); 110 | 111 | if (respawn) { 112 | child.respawn = true; 113 | } 114 | 115 | child.once('exit', code => { 116 | if (!child.respawn) process.exit(code); 117 | child.removeAllListeners(); 118 | child = undefined; 119 | }); 120 | 121 | // Listen for `required` messages and watch the required file. 122 | ipc.on(child, 'required', ({ required }) => { 123 | if (!isPaused && !isIgnored(required) && !isTooDeep(required)) watcher.add(required); 124 | }); 125 | 126 | // Upon errors, display a notification and tell the child to exit. 127 | ipc.on(child, 'error', ({ error, message, willTerminate }) => { 128 | notify(error, message, 'error'); 129 | stop(willTerminate); 130 | }); 131 | } 132 | 133 | function stop(willTerminate) { 134 | child.respawn = true; 135 | if (!willTerminate) { 136 | if (gracefulIPC) { 137 | log.info('Sending IPC: ' + JSON.stringify(gracefulIPC)); 138 | child.send(gracefulIPC); 139 | } else { 140 | child.kill('SIGTERM'); 141 | } 142 | } 143 | } 144 | 145 | // Relay SIGTERM 146 | process.on('SIGTERM', () => { 147 | if (child && child.connected) { 148 | if (gracefulIPC) { 149 | log.info('Sending IPC: ' + JSON.stringify(gracefulIPC)); 150 | child.send(gracefulIPC); 151 | } else { 152 | child.kill('SIGTERM'); 153 | } 154 | } 155 | 156 | process.exit(0); 157 | }); 158 | 159 | clearOutput(); 160 | start(); 161 | }; 162 | -------------------------------------------------------------------------------- /lib/ipc.js: -------------------------------------------------------------------------------- 1 | const cmd = 'NODE_DEV'; 2 | 3 | exports.on = (src, prop, cb) => { 4 | src.on('internalMessage', m => { 5 | if (m.cmd === cmd && prop in m) cb(m); 6 | }); 7 | }; 8 | 9 | exports.relay = src => { 10 | src.on('internalMessage', m => { 11 | if (process.connected && m.cmd === cmd) process.send(m); 12 | }); 13 | }; 14 | 15 | exports.send = m => { 16 | if (process.connected) process.send({ ...m, cmd }); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/loaders/get-format.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import { fileURLToPath } from 'url'; 3 | import { send } from './ipc.mjs'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | export async function getFormat(url, context, defaultGetFormat) { 8 | const required = url.startsWith('file://') ? fileURLToPath(url) : url; 9 | 10 | send({ required }); 11 | 12 | try { 13 | return await defaultGetFormat(url, context, defaultGetFormat); 14 | } catch (error) { 15 | if (error.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw error; 16 | return require('get-package-type')(required).then(format => { 17 | if (!['builtin', 'commonjs'].includes(format)) throw error; 18 | return { format }; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/loaders/ipc.mjs: -------------------------------------------------------------------------------- 1 | const cmd = 'NODE_DEV'; 2 | 3 | export const send = m => { 4 | if (process.connected) process.send({ ...m, cmd }); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/loaders/load.mjs: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import { fileURLToPath } from 'url'; 3 | import { send } from './ipc.mjs'; 4 | 5 | const require = createRequire(import.meta.url); 6 | 7 | export async function load(url, context, defaultLoad) { 8 | const required = url.startsWith('file://') ? fileURLToPath(url) : url; 9 | 10 | send({ required }); 11 | 12 | try { 13 | return await defaultLoad(url, context, defaultLoad); 14 | } catch (error) { 15 | if (error.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw error; 16 | return require('get-package-type')(required).then(format => { 17 | if (!['builtin', 'commonjs'].includes(format)) throw error; 18 | return { format }; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/local-path.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | module.exports = f => join(__dirname, f); 3 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | const { format } = require('util'); 2 | const dateformat = require('dateformat'); 3 | 4 | const colors = { 5 | error: '31;1', 6 | info: '36', 7 | warn: '33' 8 | }; 9 | 10 | const LOG_LEVELS = Object.keys(colors); 11 | 12 | const noop = s => s; 13 | const colorOutput = (s, c) => '\x1B[' + c + 'm' + s + '\x1B[0m'; 14 | 15 | /** 16 | * Logs a message to the console. The level is displayed in ANSI colors: 17 | * errors are bright red, warnings are yellow, and info is cyan. 18 | */ 19 | module.exports = ({ noColor, timestamp }) => { 20 | const enableColor = !(noColor || !process.stdout.isTTY); 21 | const color = enableColor ? colorOutput : noop; 22 | 23 | const log = (msg, level) => { 24 | const ts = timestamp ? color(dateformat(new Date(), timestamp), '39') + ' ' : ''; 25 | const c = colors[level.toLowerCase()] || '32'; 26 | const output = `[${color(level.toUpperCase(), c)}] ${ts}${msg}`; 27 | console.log(output); 28 | return output; 29 | }; 30 | 31 | const logFactory = level => 32 | function () { 33 | return log(format.apply(null, arguments), level); 34 | }; 35 | 36 | return LOG_LEVELS.reduce((log, level) => { 37 | log[level] = logFactory(level); 38 | return log; 39 | }, {}); 40 | }; 41 | -------------------------------------------------------------------------------- /lib/notify.js: -------------------------------------------------------------------------------- 1 | const notifier = require('node-notifier'); 2 | 3 | const localPath = require('./local-path'); 4 | 5 | const iconLevelPath = level => localPath(`../icons/node_${level}.png`); 6 | 7 | // Writes a message to the console and optionally displays a desktop notification. 8 | module.exports = (notifyEnabled, log) => { 9 | return (title = 'node-dev', message, level = 'info') => { 10 | log[level](`${title}: ${message}`); 11 | 12 | if (notifyEnabled) { 13 | notifier.notify({ 14 | title, 15 | icon: iconLevelPath(level), 16 | message 17 | }); 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/resolve-main.js: -------------------------------------------------------------------------------- 1 | const { sync: resolve } = require('resolve'); 2 | 3 | module.exports = main => { 4 | const basedir = process.cwd(); 5 | const paths = [basedir]; 6 | return resolve(main, { basedir, paths }); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/suppress-experimental-warnings.js: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/nodejs/node/issues/30810#issue-533506790 2 | 3 | module.exports = p => { 4 | const { emitWarning, emit } = p; 5 | 6 | p.emitWarning = (warning, ...args) => { 7 | if (args[0] === 'ExperimentalWarning') { 8 | return; 9 | } 10 | 11 | if (args[0] && typeof args[0] === 'object' && args[0].type === 'ExperimentalWarning') { 12 | return; 13 | } 14 | 15 | return emitWarning(warning, ...args); 16 | }; 17 | 18 | p.emit = (...args) => { 19 | if (args[1]?.name === 'ExperimentalWarning') { 20 | return; 21 | } 22 | 23 | return emit.call(p, ...args); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/wrap.js: -------------------------------------------------------------------------------- 1 | const { dirname, extname } = require('path'); 2 | const childProcess = require('child_process'); 3 | const { sync: resolve } = require('resolve'); 4 | const { isMainThread } = require('worker_threads'); 5 | 6 | const { getConfig } = require('./cfg'); 7 | const hook = require('./hook'); 8 | const { relay, send } = require('./ipc'); 9 | const resolveMain = require('./resolve-main'); 10 | const suppressExperimentalWarnings = require('./suppress-experimental-warnings'); 11 | 12 | // Experimental warnings need to be suppressed in worker threads as well, since 13 | // their process inherits the Node arguments from the main thread. 14 | suppressExperimentalWarnings(process); 15 | 16 | // When using worker threads, each thread appears to require this file through 17 | // the shared Node arguments (--require), so filter them out here and only run 18 | // on the main thread. 19 | if (!isMainThread) return; 20 | 21 | const script = process.argv[1]; 22 | const { extensions, fork, vm } = getConfig(script); 23 | 24 | if (process.env.NODE_DEV_PRELOAD) { 25 | require(process.env.NODE_DEV_PRELOAD); 26 | } 27 | 28 | // We want to exit on SIGTERM, but defer to existing SIGTERM handlers. 29 | process.once('SIGTERM', () => process.listenerCount('SIGTERM') || process.exit(0)); 30 | 31 | if (fork) { 32 | // Overwrite child_process.fork() so that we can hook into forked processes 33 | // too. We also need to relay messages about required files to the parent. 34 | const originalFork = childProcess.fork; 35 | childProcess.fork = (modulePath, args, options) => { 36 | const child = originalFork(modulePath, args, options); 37 | relay(child); 38 | return child; 39 | }; 40 | } 41 | 42 | // Error handler that displays a notification and logs the stack to stderr: 43 | process.on('uncaughtException', err => { 44 | // Sometimes uncaught exceptions are not errors 45 | const { message, name, stack } = 46 | err instanceof Error ? err : new Error(`uncaughtException ${err}`); 47 | 48 | console.error(stack); 49 | 50 | // If there's a custom uncaughtException handler expect it to terminate 51 | // the process. 52 | const willTerminate = process.listenerCount('uncaughtException') > 1; 53 | 54 | send({ error: name, message, willTerminate }); 55 | }); 56 | 57 | // Hook into require() and notify the parent process about required files 58 | hook(vm, required => send({ required })); 59 | 60 | // Check if a module is registered for this extension 61 | const main = resolveMain(script); 62 | const ext = extname(main).slice(1); 63 | const mod = extensions[ext]; 64 | const basedir = dirname(main); 65 | 66 | // Support extensions where 'require' returns a function that accepts options 67 | if (typeof mod === 'object' && mod.name) { 68 | const fn = require(resolve(mod.name, { basedir })); 69 | if (typeof fn === 'function' && mod.options) { 70 | // require returned a function, call it with options 71 | fn(mod.options); 72 | } 73 | } else if (typeof mod === 'string') { 74 | require(resolve(mod, { basedir })); 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-dev", 3 | "version": "8.0.0", 4 | "description": "Restarts your app when files are modified", 5 | "keywords": [ 6 | "restart", 7 | "reload", 8 | "supervisor", 9 | "monitor", 10 | "watch" 11 | ], 12 | "author": "Felix Gnass (https://github.com/fgnass)", 13 | "contributors": [ 14 | "Daniel Gasienica (https://github.com/gasi)", 15 | "Bjorn Stromberg (https://bjornstar.com)" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "http://github.com/fgnass/node-dev.git" 20 | }, 21 | "license": "MIT", 22 | "bin": { 23 | "node-dev": "bin/node-dev" 24 | }, 25 | "main": "./lib", 26 | "engines": { 27 | "node": ">=14" 28 | }, 29 | "scripts": { 30 | "lint": "eslint lib test bin/node-dev", 31 | "test": "node test", 32 | "prepare": "husky install" 33 | }, 34 | "dependencies": { 35 | "dateformat": "^3.0.3", 36 | "dynamic-dedupe": "^0.3.0", 37 | "filewatcher": "~3.0.0", 38 | "get-package-type": "^0.1.0", 39 | "minimist": "^1.2.6", 40 | "node-notifier": "^8.0.1", 41 | "resolve": "^1.22.0", 42 | "semver": "^7.3.7" 43 | }, 44 | "devDependencies": { 45 | "@types/node": "^18.11.18", 46 | "eslint": "^8.30.0", 47 | "eslint-plugin-import": "^2.26.0", 48 | "husky": "^8.0.2", 49 | "lint-staged": "^13.1.0", 50 | "prettier": "^2.6.2", 51 | "tap": "^16.3.2", 52 | "tap-xunit": "^2.4.1", 53 | "touch": "^3.1.0", 54 | "ts-node": "^10.7.0", 55 | "typescript": "^4.6.3" 56 | }, 57 | "lint-staged": { 58 | "*.{js,mjs}": "eslint --cache --fix", 59 | "*.{js,md}": "prettier --write" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const cli = require('../lib/cli.js'); 4 | 5 | tap.test('notify is enabled by default', t => { 6 | const { 7 | opts: { notify } 8 | } = cli(['node', 'bin/node-dev', 'test']); 9 | 10 | t.equal(notify, true); 11 | t.end(); 12 | }); 13 | 14 | tap.test('--no-notify', t => { 15 | const { 16 | opts: { notify } 17 | } = cli(['node', 'bin/node-dev', '--no-notify', 'test']); 18 | 19 | t.equal(notify, false); 20 | t.end(); 21 | }); 22 | 23 | tap.test('--notify=false', t => { 24 | const { 25 | opts: { notify } 26 | } = cli(['node', 'bin/node-dev', '--notify=false', 'test']); 27 | 28 | t.equal(notify, false); 29 | t.end(); 30 | }); 31 | 32 | tap.test('--notify', t => { 33 | const { 34 | opts: { notify } 35 | } = cli(['node', 'bin/node-dev', '--notify', 'test']); 36 | 37 | t.equal(notify, true); 38 | t.end(); 39 | }); 40 | 41 | tap.test('--notify=true', t => { 42 | const { 43 | opts: { notify } 44 | } = cli(['node', 'bin/node-dev', '--notify=true', 'test']); 45 | 46 | t.equal(notify, true); 47 | t.end(); 48 | }); 49 | 50 | tap.test('notify can be disabled by .node-dev.json', t => { 51 | const { 52 | opts: { notify } 53 | } = cli(['node', 'bin/node-dev', 'test/fixture/server.js']); 54 | 55 | t.equal(notify, false); 56 | t.end(); 57 | }); 58 | 59 | tap.test('cli overrides .node-dev.json from false to true', t => { 60 | const { 61 | opts: { notify } 62 | } = cli(['node', 'bin/node-dev', '--notify=true', 'test/fixture/server.js']); 63 | 64 | t.equal(notify, true); 65 | t.end(); 66 | }); 67 | 68 | tap.test('-r ts-node/register --inspect test/fixture/server.js', t => { 69 | const argv = 'node bin/node-dev -r ts-node/register --inspect test/fixture/server.js'.split(' '); 70 | const { nodeArgs } = cli(argv); 71 | t.same(nodeArgs, ['--inspect', '--require=ts-node/register']); 72 | t.end(); 73 | }); 74 | 75 | tap.test('--inspect -r ts-node/register test/fixture/server.js', t => { 76 | const argv = 'node bin/node-dev --inspect -r ts-node/register test/fixture/server.js'.split(' '); 77 | const { nodeArgs } = cli(argv); 78 | t.same(nodeArgs, ['--inspect', '--require=ts-node/register']); 79 | t.end(); 80 | }); 81 | 82 | tap.test('--expose_gc gc.js foo', t => { 83 | const argv = 'node bin/node-dev --expose_gc test/fixture/gc.js foo'.split(' '); 84 | const { nodeArgs } = cli(argv); 85 | t.same(nodeArgs, ['--expose_gc']); 86 | t.end(); 87 | }); 88 | 89 | tap.test('--preserve-symlinks test', t => { 90 | const argv = 'node bin/node-dev --preserve-symlinks test'.split(' '); 91 | const { nodeArgs } = cli(argv); 92 | t.same(nodeArgs, ['--preserve-symlinks']); 93 | t.end(); 94 | }); 95 | 96 | tap.test('clear is not enabled by default', t => { 97 | const { 98 | opts: { clear } 99 | } = cli(['node', 'bin/node-dev', 'test']); 100 | 101 | t.notOk(clear); 102 | t.end(); 103 | }); 104 | 105 | tap.test('--clear enables clear', t => { 106 | const { 107 | opts: { clear } 108 | } = cli(['node', 'bin/node-dev', '--clear', 'test']); 109 | 110 | t.ok(clear); 111 | t.end(); 112 | }); 113 | 114 | tap.test('interval default', t => { 115 | const { 116 | opts: { interval } 117 | } = cli(['node', 'bin/node-dev', 'test']); 118 | 119 | t.equal(interval, 1000); 120 | t.end(); 121 | }); 122 | 123 | tap.test('--interval=2000', t => { 124 | const { 125 | opts: { interval } 126 | } = cli(['node', 'bin/node-dev', '--interval=2000', 'test']); 127 | 128 | t.equal(interval, 2000); 129 | t.end(); 130 | }); 131 | 132 | tap.test('debounce default', t => { 133 | const { 134 | opts: { debounce } 135 | } = cli(['node', 'bin/node-dev', 'test']); 136 | 137 | t.equal(debounce, 10); 138 | t.end(); 139 | }); 140 | 141 | tap.test('--debounce=2000', t => { 142 | const { 143 | opts: { debounce } 144 | } = cli(['node', 'bin/node-dev', '--debounce=2000', 'test']); 145 | 146 | t.equal(debounce, 2000); 147 | t.end(); 148 | }); 149 | 150 | tap.test('--require source-map-support/register', t => { 151 | const { nodeArgs } = cli([ 152 | 'node', 153 | 'bin/node-dev', 154 | '--require', 155 | 'source-map-support/register', 156 | 'test' 157 | ]); 158 | 159 | t.same(nodeArgs, ['--require=source-map-support/register']); 160 | t.end(); 161 | }); 162 | 163 | tap.test('--require=source-map-support/register', t => { 164 | const { nodeArgs } = cli([ 165 | 'node', 166 | 'bin/node-dev', 167 | '--require=source-map-support/register', 168 | 'test' 169 | ]); 170 | 171 | t.same(nodeArgs, ['--require=source-map-support/register']); 172 | t.end(); 173 | }); 174 | 175 | tap.test('-r source-map-support/register', t => { 176 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-r', 'source-map-support/register', 'test']); 177 | 178 | t.same(nodeArgs, ['--require=source-map-support/register']); 179 | t.end(); 180 | }); 181 | 182 | tap.test('-r=source-map-support/register', t => { 183 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-r=source-map-support/register', 'test']); 184 | 185 | t.same(nodeArgs, ['--require=source-map-support/register']); 186 | t.end(); 187 | }); 188 | 189 | tap.test('--inspect=127.0.0.1:12345', t => { 190 | const { nodeArgs } = cli(['node', 'bin/node-dev', '--inspect=127.0.0.1:12345', 'test']); 191 | 192 | t.same(nodeArgs, ['--inspect=127.0.0.1:12345']); 193 | t.end(); 194 | }); 195 | 196 | tap.test('--inspect', t => { 197 | const { nodeArgs } = cli(['node', 'bin/node-dev', '--inspect', 'test']); 198 | 199 | t.same(nodeArgs, ['--inspect']); 200 | t.end(); 201 | }); 202 | 203 | tap.test('--no-warnings', t => { 204 | const { nodeArgs } = cli(['node', 'bin/node-dev', '--no-warnings', 'test']); 205 | 206 | t.same(nodeArgs, ['--no-warnings']); 207 | t.end(); 208 | }); 209 | 210 | tap.test('--require source-map-support/register --require ts-node/register', t => { 211 | const { nodeArgs } = cli([ 212 | 'node', 213 | 'bin/node-dev', 214 | '--require', 215 | 'source-map-support/register', 216 | '--require', 217 | 'ts-node/register', 218 | 'test' 219 | ]); 220 | 221 | t.same(nodeArgs, ['--require=source-map-support/register', '--require=ts-node/register']); 222 | t.end(); 223 | }); 224 | 225 | // This should display usage information at some point 226 | tap.test('No script or option should fail', t => { 227 | t.throws(() => cli(['node', 'bin/node-dev'])); 228 | t.end(); 229 | }); 230 | 231 | tap.test('Just an option should fail', t => { 232 | t.throws(() => cli(['node', 'bin/node-dev', '--option'])); 233 | t.end(); 234 | }); 235 | 236 | tap.test('Just an option with a value should fail', t => { 237 | t.throws(() => cli(['node', 'bin/node-dev', '--option=value'])); 238 | t.end(); 239 | }); 240 | 241 | tap.test('An unknown argument with a value instead of a script should fail.', t => { 242 | t.throws(() => cli(['node', 'bin/node-dev', '--unknown-arg', 'value'])); 243 | t.end(); 244 | }); 245 | 246 | tap.test('An unknown argument with a value', t => { 247 | const { nodeArgs } = cli(['node', 'bin/node-dev', '--unknown-arg=value', 'test']); 248 | 249 | t.same(nodeArgs, ['--unknown-arg=value']); 250 | t.end(); 251 | }); 252 | 253 | tap.test('An unknown argument without a value can use -- to delimit', t => { 254 | // use -- to delimit the end of options 255 | const { nodeArgs } = cli(['node', 'bin/node-dev', '--unknown-arg', '--', 'test']); 256 | 257 | t.same(nodeArgs, ['--unknown-arg']); 258 | t.end(); 259 | }); 260 | 261 | tap.test('Single dash with value', t => { 262 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-u', 'value', 'test']); 263 | 264 | t.same(nodeArgs, ['-u=value']); 265 | t.end(); 266 | }); 267 | 268 | tap.test('Single dash with = and value', t => { 269 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-u=value', 'test']); 270 | 271 | t.same(nodeArgs, ['-u=value']); 272 | t.end(); 273 | }); 274 | 275 | tap.test('Single dash without value should fail', t => { 276 | t.throws(() => cli(['node', 'bin/node-dev', '-u', 'test'])); 277 | t.end(); 278 | }); 279 | 280 | tap.test('Single dash without value can use -- to delimit', t => { 281 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-u', '--', 'test']); 282 | 283 | t.same(nodeArgs, ['-u']); 284 | t.end(); 285 | }); 286 | 287 | tap.test('Repeated single dash', t => { 288 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-u=value1', '-u=value2', 'test']); 289 | 290 | t.same(nodeArgs, ['-u=value1', '-u=value2']); 291 | t.end(); 292 | }); 293 | 294 | tap.test('Repeated single dash without =', t => { 295 | const { nodeArgs } = cli(['node', 'bin/node-dev', '-u', 'value1', '-u', 'value2', 'test']); 296 | 297 | t.same(nodeArgs, ['-u=value1', '-u=value2']); 298 | t.end(); 299 | }); 300 | 301 | tap.test( 302 | 'All command-line arguments that are not `node-dev` options are passed on to the `node` process.', 303 | t => { 304 | // Everything except clear gets passed to node. 305 | // Don't forget to use -- to delimit! 306 | const argv = 307 | 'node bin/node-dev --all --command-line --arguments --clear --that --are --not --node-dev --options -- test'.split( 308 | ' ' 309 | ); 310 | const { nodeArgs } = cli(argv); 311 | 312 | t.same(nodeArgs, [ 313 | '--all', 314 | '--command-line', 315 | '--arguments', 316 | '--that', 317 | '--are', 318 | '--not', 319 | '--node-dev', 320 | '--options' 321 | ]); 322 | t.end(); 323 | } 324 | ); 325 | -------------------------------------------------------------------------------- /test/fixture/.node-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "notify": false, 3 | "ignore": ["./ignored-module.js"], 4 | "extensions": { 5 | "ts": "ts-node/register" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/argv.js: -------------------------------------------------------------------------------- 1 | console.log(process.argv); 2 | -------------------------------------------------------------------------------- /test/fixture/builtin.mjs: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | console.log(join('hello', 'world')); 4 | -------------------------------------------------------------------------------- /test/fixture/catch-no-such-module.js: -------------------------------------------------------------------------------- 1 | try { 2 | require('some_module_that_does_not_exist'); 3 | } catch (err) { 4 | console.log('Caught', err); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/cluster.js: -------------------------------------------------------------------------------- 1 | const { disconnect, fork, isMaster, isWorker } = require('cluster'); 2 | 3 | const createWorker = i => { 4 | const worker = fork(); 5 | 6 | worker.on('message', msg => { 7 | console.log(`Message from worker ${i}: ${msg}`); 8 | }); 9 | 10 | worker.on('exit', code => { 11 | console.log(`Worker ${i} exited with code: ${code}`); 12 | }); 13 | 14 | return worker; 15 | }; 16 | 17 | if (isWorker) { 18 | const server = require('./server'); 19 | 20 | process.on('disconnect', () => { 21 | console.log(`${process.pid} disconnect received, shutting down`); 22 | if (server.listening) server.close(); 23 | }); 24 | 25 | process.send('Hello'); 26 | } 27 | 28 | if (isMaster) { 29 | for (let i = 0; i < 2; i += 1) { 30 | console.log('Forking worker', i); 31 | createWorker(i); 32 | } 33 | 34 | process.once('SIGTERM', () => { 35 | console.log('Master received SIGTERM'); 36 | disconnect(() => { 37 | console.log('All workers disconnected.'); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/fixture/echo.js: -------------------------------------------------------------------------------- 1 | process.stdin.resume(); 2 | process.stdin.pipe(process.stdout); 3 | -------------------------------------------------------------------------------- /test/fixture/ecma-script-module-package/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/ecma-script-module-package/index.js: -------------------------------------------------------------------------------- 1 | import message from './message.js'; 2 | 3 | console.log(message); 4 | 5 | // So it doesn't immediately exit. 6 | setTimeout(() => {}, 10000); 7 | -------------------------------------------------------------------------------- /test/fixture/ecma-script-module-package/message.js: -------------------------------------------------------------------------------- 1 | export default 'Please touch ecma-script-module-package/message.js now'; 2 | -------------------------------------------------------------------------------- /test/fixture/ecma-script-module-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/ecma-script-modules.mjs: -------------------------------------------------------------------------------- 1 | import message from './message.mjs'; 2 | 3 | console.log(message); 4 | 5 | // So it doesn't immediately exit. 6 | setTimeout(() => {}, 10000); 7 | -------------------------------------------------------------------------------- /test/fixture/env.js: -------------------------------------------------------------------------------- 1 | console.log('NODE_ENV:', process.env.NODE_ENV); 2 | -------------------------------------------------------------------------------- /test/fixture/error-null.js: -------------------------------------------------------------------------------- 1 | require('./message'); 2 | 3 | // eslint-disable-next-line no-throw-literal 4 | throw null; 5 | -------------------------------------------------------------------------------- /test/fixture/error.js: -------------------------------------------------------------------------------- 1 | require('./message'); 2 | 3 | /* eslint-disable no-undef */ 4 | intentionally_undefined(); 5 | -------------------------------------------------------------------------------- /test/fixture/exit.js: -------------------------------------------------------------------------------- 1 | process.exit(101); 2 | -------------------------------------------------------------------------------- /test/fixture/experimental-specifier-resolution/index.mjs: -------------------------------------------------------------------------------- 1 | export default 'experimental-specifier-resolution'; 2 | -------------------------------------------------------------------------------- /test/fixture/extension-options/.node-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": { 3 | "js": { 4 | "name": "./extension.js", 5 | "options": { 6 | "test": true 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/extension-options/extension.js: -------------------------------------------------------------------------------- 1 | // Example extension/transpiler with options 2 | module.exports = function (options) { 3 | if (typeof options !== 'object') { 4 | throw new Error('Expected options to be an object'); 5 | } 6 | if (options.test !== true) { 7 | throw new Error('Expected options.test to be true'); 8 | } 9 | console.log(options); 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixture/extension-options/index.js: -------------------------------------------------------------------------------- 1 | const message = require('../message.js'); 2 | 3 | console.log(message); 4 | 5 | // So it doesn't immediately exit. 6 | setTimeout(() => {}, 10000); 7 | -------------------------------------------------------------------------------- /test/fixture/gc.js: -------------------------------------------------------------------------------- 1 | console.log(process.argv[2], typeof global.gc); 2 | -------------------------------------------------------------------------------- /test/fixture/ignored-module.js: -------------------------------------------------------------------------------- 1 | module.exports = 'Ignored file — `touch`ing it should not restart server.'; 2 | -------------------------------------------------------------------------------- /test/fixture/ipc-server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | 3 | const message = require('./message'); 4 | 5 | const server = createServer((req, res) => { 6 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 7 | res.write(message); 8 | res.end('\n'); 9 | }); 10 | 11 | server 12 | .on('listening', () => { 13 | const { address, port } = server.address(); 14 | console.log(`Server listening on ${address}:${port}`); 15 | console.log(message); 16 | }) 17 | .listen(0); 18 | 19 | process.on('message', data => { 20 | if (data === 'node-dev:restart') { 21 | console.log('ipc-server.js - IPC received'); 22 | server.close(); 23 | } 24 | }); 25 | 26 | process.once('beforeExit', () => console.log('exit')); 27 | 28 | process.once('SIGTERM', () => { 29 | if (server.listening) server.close(); 30 | }); 31 | -------------------------------------------------------------------------------- /test/fixture/log.js: -------------------------------------------------------------------------------- 1 | console.log(require('./message')); 2 | -------------------------------------------------------------------------------- /test/fixture/main.js: -------------------------------------------------------------------------------- 1 | process.exit(module === require.main ? 0 : 1); 2 | -------------------------------------------------------------------------------- /test/fixture/message.js: -------------------------------------------------------------------------------- 1 | module.exports = 'Please touch message.js now'; 2 | -------------------------------------------------------------------------------- /test/fixture/message.mjs: -------------------------------------------------------------------------------- 1 | export default 'Please touch message.mjs now'; 2 | -------------------------------------------------------------------------------- /test/fixture/modify-extensions.js: -------------------------------------------------------------------------------- 1 | require.extensions.bogus = undefined; 2 | 3 | console.log('extensions modified'); 4 | -------------------------------------------------------------------------------- /test/fixture/no-such-module.js: -------------------------------------------------------------------------------- 1 | require('some_module_that_does_not_exist'); 2 | -------------------------------------------------------------------------------- /test/fixture/pid.js: -------------------------------------------------------------------------------- 1 | const server = require('http').createServer().listen(0); 2 | console.log(process.pid); 3 | process.once('SIGTERM', () => server.close()); 4 | -------------------------------------------------------------------------------- /test/fixture/resolution.mjs: -------------------------------------------------------------------------------- 1 | import resolution from './experimental-specifier-resolution'; 2 | import message from './message'; 3 | 4 | setTimeout(() => {}, 10000); 5 | 6 | console.log(resolution); 7 | console.log(message); 8 | -------------------------------------------------------------------------------- /test/fixture/server.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | 3 | const message = require('./message'); 4 | 5 | // Changes to this module should not cause a server restart: 6 | require('./ignored-module'); 7 | 8 | const server = createServer((req, res) => { 9 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 10 | res.write(message); 11 | res.end('\n'); 12 | }); 13 | 14 | server 15 | .once('listening', () => { 16 | const { address, port } = server.address(); 17 | console.log('Server listening on %s:%s', address, port); 18 | console.log(message); 19 | }) 20 | .listen(0); 21 | 22 | process.once('SIGTERM', () => { 23 | if (server.listening) server.close(); 24 | }); 25 | 26 | process.once('beforeExit', () => console.log('exit')); 27 | 28 | module.exports = server; 29 | -------------------------------------------------------------------------------- /test/fixture/typescript-module/index.ts: -------------------------------------------------------------------------------- 1 | // We should not be able to load typescript files as ESModules 2 | export default ['test', 'data'] as const 3 | -------------------------------------------------------------------------------- /test/fixture/typescript-module/package.json: -------------------------------------------------------------------------------- 1 | { "type": "module" } 2 | -------------------------------------------------------------------------------- /test/fixture/typescript/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | import message from './message'; 4 | 5 | const server = createServer((req, res) => { 6 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 7 | res.write(message); 8 | res.end('\n'); 9 | }); 10 | 11 | server.once('listening', () => { 12 | const addressInfo = server.address() || 'unknown'; 13 | const address = typeof addressInfo == 'string' ? 14 | addressInfo : `${addressInfo.address}:${addressInfo.port}`; 15 | 16 | console.log(`Server listening on ${address}`); 17 | console.log(message); 18 | }).listen(0); 19 | 20 | process.once('SIGTERM', () => { 21 | if (server.listening) { 22 | server.close(); 23 | } 24 | }); 25 | 26 | process.once('beforeExit', () => console.log('exit')); 27 | 28 | export default server; 29 | -------------------------------------------------------------------------------- /test/fixture/typescript/message.ts: -------------------------------------------------------------------------------- 1 | export default 'Please touch message.ts now'; 2 | -------------------------------------------------------------------------------- /test/fixture/uncaught-exception-handler.js: -------------------------------------------------------------------------------- 1 | process.on('uncaughtException', e => { 2 | setTimeout(() => console.log('async', e), 100); 3 | }); 4 | 5 | // eslint-disable-next-line no-undef 6 | foo(); // undefined / throws exception 7 | -------------------------------------------------------------------------------- /test/fixture/vmtest.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs'); 2 | const server = require('http').createServer().listen(0); 3 | const { join } = require('path'); 4 | const vm = require('vm'); 5 | 6 | const file = join(__dirname, 'log.js'); 7 | const str = readFileSync(file, 'utf8'); 8 | 9 | if (process.argv.length > 2 && process.argv[2] === 'nofile') { 10 | vm.runInNewContext(str, { module: {}, require: require, console: console }); 11 | } else { 12 | vm.runInNewContext(str, { module: {}, require: require, console: console }, file); 13 | } 14 | 15 | process.once('SIGTERM', () => server.close()); 16 | 17 | process.once('beforeExit', () => console.log('exit')); 18 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./cli'); 2 | require('./log'); 3 | require('./run'); 4 | require('./spawn'); 5 | -------------------------------------------------------------------------------- /test/log.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { defaultConfig } = require('../lib/cfg'); 4 | const logFactory = require('../lib/log'); 5 | 6 | const noColorCfg = { ...defaultConfig, noColor: true }; 7 | 8 | tap.test('log.info', t => { 9 | const log = logFactory(noColorCfg); 10 | t.match(log.info('hello'), /\[INFO\] \d{2}:\d{2}:\d{2} hello/); 11 | t.end(); 12 | }); 13 | 14 | tap.test('log.warn', t => { 15 | const log = logFactory(noColorCfg); 16 | t.match(log.warn('a warning'), /\[WARN\] \d{2}:\d{2}:\d{2} a warning/); 17 | t.end(); 18 | }); 19 | 20 | tap.test('log.error', t => { 21 | const log = logFactory(noColorCfg); 22 | t.match(log.error('an error'), /\[ERROR\] \d{2}:\d{2}:\d{2} an error/); 23 | t.end(); 24 | }); 25 | 26 | tap.test('Disable the timestamp', t => { 27 | const log = logFactory({ ...noColorCfg, timestamp: false }); 28 | t.match(log.info('no timestamp'), /\[INFO\] no timestamp/); 29 | t.end(); 30 | }); 31 | 32 | tap.test('Custom timestamp', t => { 33 | const log = logFactory({ ...noColorCfg, timestamp: 'yyyy-mm-dd HH:MM:ss' }); 34 | t.match(log.error('an error'), /\[ERROR\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} an error/); 35 | t.end(); 36 | }); 37 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('./utils'); 4 | 5 | const run = (cmd, exit) => { 6 | return spawn(cmd, out => { 7 | let touched = false; 8 | if (!touched && out.match(/touch message\.js/)) { 9 | touchFile('message.js'); 10 | touched = true; 11 | return out2 => { 12 | if (out2.match(/Restarting/)) { 13 | return { exit }; 14 | } 15 | }; 16 | } 17 | }); 18 | }; 19 | 20 | tap.test('Restart the server', t => { 21 | run('server.js', t.end.bind(t)); 22 | }); 23 | 24 | tap.test('Supports vm functions', t => { 25 | run('vmtest.js', t.end.bind(t)); 26 | }); 27 | 28 | tap.test('Supports vm functions with missing file argument', t => { 29 | run('vmtest.js nofile', t.end.bind(t)); 30 | }); 31 | -------------------------------------------------------------------------------- /test/spawn/argv.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should not show up in argv', t => { 6 | spawn('argv.js foo', out => { 7 | let argv; 8 | try { 9 | argv = JSON.parse(out.replace(/'/g, '"')); 10 | } catch (e) { 11 | // failed to parse, that's ok. 12 | } 13 | if (argv) { 14 | t.match(argv[0], /.*?node(js|\.exe)?$/); 15 | t.match(argv[1], /.*[\\/]argv\.js$/); 16 | t.equal(argv[2], 'foo'); 17 | 18 | return { exit: t.end.bind(t) }; 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/spawn/builtin.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('can import builtin modules', t => { 6 | spawn('builtin.mjs', out => { 7 | if (out.match(/^hello[/\\]world/)) return { exit: t.end.bind(t) }; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spawn/caught.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should ignore caught errors', t => { 6 | spawn('catch-no-such-module.js', out => { 7 | if (out.match(/^Caught Error/)) return { exit: t.end.bind(t) }; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spawn/clear.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | const { control } = require('../../lib/clear'); 6 | 7 | const reClear = new RegExp(control); 8 | 9 | tap.test('--clear', t => { 10 | spawn('--clear server.js', out => { 11 | if (reClear.test(out)) { 12 | return out2 => { 13 | if (out2.match(/touch message.js/)) { 14 | touchFile('message.js'); 15 | return out3 => { 16 | if (out3.match(/Restarting/)) { 17 | return { exit: t.end.bind(t) }; 18 | } 19 | }; 20 | } 21 | }; 22 | } 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/spawn/cli-require.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Supports require from the command-line (ts-node/register)', t => { 6 | spawn('--require=ts-node/register typescript/index.ts', out => { 7 | if (out.match(/touch message.ts/)) { 8 | touchFile('typescript', 'message.ts'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 12 | return { exit: t.end.bind(t) }; 13 | } 14 | }; 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/spawn/cluster.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Restart the cluster', t => { 6 | if (process.platform === 'win32') { 7 | t.pass('Restart the cluster', { skip: 'Signals are not supported on Windows' }); 8 | return t.end(); 9 | } 10 | 11 | spawn('cluster.js', out => { 12 | if (out.match(/touch message\.js/m)) { 13 | touchFile('message.js'); 14 | return out2 => { 15 | if (out2.match(/Restarting/m)) { 16 | return out3 => { 17 | if (out3.match(/All workers disconnected/m)) { 18 | let shuttingDown = false; 19 | return out4 => { 20 | if (out4.match(/touch message\.js/) && !shuttingDown) { 21 | shuttingDown = true; 22 | return { exit: t.end.bind(t) }; 23 | } 24 | }; 25 | } 26 | }; 27 | } 28 | }; 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/spawn/conceal.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should conceal the wrapper', t => { 6 | // require.main should be main.js not wrap.js! 7 | spawn('main.js').on('exit', code => { 8 | t.equal(code, 0); 9 | t.end(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/spawn/errors.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('should handle errors', t => { 6 | spawn('error.js', out => { 7 | if (out.match(/ERROR/)) { 8 | touchFile('message.js'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | return { exit: t.end.bind(t) }; 12 | } 13 | }; 14 | } 15 | }); 16 | }); 17 | 18 | tap.test('should handle null errors', t => { 19 | spawn('error-null.js', out => { 20 | if (out.match(/ERROR/)) { 21 | touchFile('message.js'); 22 | return out2 => { 23 | if (out2.match(/Restarting/)) { 24 | return { exit: t.end.bind(t) }; 25 | } 26 | }; 27 | } 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/spawn/esmodule.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Supports ECMAScript modules with experimental-specifier-resolution', t => { 6 | spawn('--experimental-specifier-resolution=node resolution.mjs', out => { 7 | if (out.match(/touch message.js/)) { 8 | touchFile('message.js'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 12 | return { exit: t.end.bind(t) }; 13 | } 14 | }; 15 | } 16 | }); 17 | }); 18 | 19 | tap.test('Supports ECMAScript modules', t => { 20 | spawn('ecma-script-modules.mjs', out => { 21 | if (out.match(/touch message.mjs/)) { 22 | touchFile('message.mjs'); 23 | return out2 => { 24 | if (out2.match(/Restarting/)) { 25 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 26 | return { exit: t.end.bind(t) }; 27 | } 28 | }; 29 | } 30 | }); 31 | }); 32 | 33 | tap.test('Supports ECMAScript module packages', t => { 34 | spawn('ecma-script-module-package/index.js', out => { 35 | if (out.match(/touch ecma-script-module-package\/message.js/)) { 36 | touchFile('ecma-script-module-package/message.js'); 37 | return out2 => { 38 | if (out2.match(/Restarting/)) { 39 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 40 | return { exit: t.end.bind(t) }; 41 | } 42 | }; 43 | } 44 | }); 45 | }); 46 | 47 | tap.test('We can hide the experimental warning by passing --no-warnings', t => { 48 | spawn('--no-warnings ecma-script-modules.mjs', out => { 49 | if (out.match(/ExperimentalWarning/)) return t.fail('Should not log an ExperimentalWarning'); 50 | 51 | if (out.match(/touch message.mjs/)) { 52 | touchFile('message.mjs'); 53 | return out2 => { 54 | if (out2.match(/Restarting/)) { 55 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 56 | return { exit: t.end.bind(t) }; 57 | } 58 | }; 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/spawn/exit-code.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should pass through the exit code', t => { 6 | spawn('exit.js').on('exit', code => { 7 | t.equal(code, 101); 8 | t.end(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/spawn/experimental-warnings.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | const { spawn } = require('../utils'); 3 | 4 | tap.test('Should suppress experimental warning spam', t => { 5 | spawn('env.js', out => { 6 | if (out.match(/ExperimentalWarning/)) return t.fail('Should not log an ExperimentalWarning'); 7 | 8 | return { 9 | exit: t.end.bind(t) 10 | }; 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/spawn/expose-gc.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should pass unknown args to node binary', t => { 6 | spawn('--expose_gc gc.js foo', out => { 7 | if (out.trim() === 'foo function') return { exit: t.end.bind(t) }; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spawn/extension-options.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should pass options to extensions according to .node-dev.json', t => { 6 | spawn('extension-options', out => { 7 | if (out.match(/\{ test: true \}/)) return { exit: t.end.bind(t) }; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spawn/graceful-ipc.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('should send IPC message when configured', t => { 6 | spawn('--graceful_ipc=node-dev:restart ipc-server.js', out => { 7 | if (out.match(/touch message.js/)) { 8 | touchFile('message.js'); 9 | let shuttingDown = false; 10 | return out2 => { 11 | if (out2.match(/IPC received/) && !shuttingDown) { 12 | shuttingDown = true; 13 | return { exit: t.end.bind(t) }; 14 | } 15 | }; 16 | } 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/spawn/index.js: -------------------------------------------------------------------------------- 1 | require('./argv'); 2 | require('./caught'); 3 | require('./clear'); 4 | require('./cli-require'); 5 | require('./cluster'); 6 | require('./conceal'); 7 | require('./errors'); 8 | require('./esmodule'); 9 | require('./exit-code'); 10 | require('./experimental-warnings'); 11 | require('./expose-gc'); 12 | require('./extension-options'); 13 | require('./graceful-ipc'); 14 | require('./inspect'); 15 | require('./kill-fork'); 16 | require('./no-such-module'); 17 | require('./node-env'); 18 | require('./relay-stdin'); 19 | require('./require-extensions'); 20 | require('./restart-twice'); 21 | require('./sigterm'); 22 | require('./timestamps'); 23 | require('./typescript'); 24 | require('./uncaught'); 25 | -------------------------------------------------------------------------------- /test/spawn/inspect.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Supports --inspect', t => { 6 | spawn('--inspect server.js', out => { 7 | if (out.match(/Debugger listening on/)) { 8 | return out2 => { 9 | if (out2.match(/touch message.js/)) { 10 | touchFile('message.js'); 11 | return out3 => { 12 | if (out3.match(/Restarting/)) { 13 | return { exit: t.end.bind(t) }; 14 | } 15 | }; 16 | } 17 | }; 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spawn/kill-fork.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should kill the forked processes', t => { 6 | spawn('pid.js', out => { 7 | const pid = parseInt(out, 10); 8 | 9 | if (!Number.isNaN(pid)) 10 | return { 11 | exit: () => { 12 | setTimeout(() => { 13 | try { 14 | process.kill(pid); 15 | t.fail('child must no longer run'); 16 | } catch (e) { 17 | t.end(); 18 | } 19 | }, 500); 20 | } 21 | }; 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/spawn/no-such-module.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should watch if no such module', t => { 6 | let passed = false; 7 | spawn('no-such-module.js', out => { 8 | if (!passed && out.match(/ERROR/)) { 9 | passed = true; 10 | return { exit: t.end.bind(t) }; 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/spawn/node-env.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should *not* set NODE_ENV', t => { 6 | spawn('env.js', out => { 7 | if (out.startsWith('NODE_ENV:')) { 8 | t.notMatch(out, /development/); 9 | return { exit: t.end.bind(t) }; 10 | } 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/spawn/relay-stdin.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should relay stdin', t => { 6 | spawn('echo.js', out => { 7 | if (out === 'foo') return { exit: t.end.bind(t) }; 8 | }).stdin.write('foo'); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spawn/require-extensions.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should be resistant to breaking `require.extensions`', t => { 6 | spawn('modify-extensions.js', out => { 7 | t.notOk(/TypeError/.test(out)); 8 | if (out.match('extensions modified')) return { exit: t.end.bind(t) }; 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/spawn/restart-twice.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('should restart the server twice', t => { 6 | spawn('server.js', out => { 7 | if (out.match(/touch message.js/)) { 8 | touchFile('message.js'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | touchFile('message.js'); 12 | return out3 => { 13 | if (out3.match(/Restarting/)) { 14 | return { exit: t.end.bind(t) }; 15 | } 16 | }; 17 | } 18 | }; 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/spawn/sigterm.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('should allow graceful shutdowns', t => { 6 | if (process.platform === 'win32') { 7 | t.pass('should allow graceful shutdowns', { skip: 'Signals are not supported on Windows' }); 8 | t.end(); 9 | } else { 10 | spawn('server.js', out => { 11 | if (out.match(/touch message.js/)) { 12 | touchFile('message.js'); 13 | return out2 => { 14 | if (out2.match(/exit/)) { 15 | return { exit: t.end.bind(t) }; 16 | } 17 | }; 18 | } 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/spawn/timestamps.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Logs timestamp by default', t => { 6 | spawn('server.js', out => { 7 | if (out.match(/touch message.js/)) { 8 | touchFile('message.js'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 12 | return { exit: t.end.bind(t) }; 13 | } 14 | }; 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/spawn/typescript-module.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test( 6 | 'Gives ERR_UNKNOWN_FILE_EXTENSION when loading typescript files and package type is "module"', 7 | t => { 8 | spawn('typescript-module/index.ts', out => { 9 | if (out.match(/ERR_UNKNOWN_FILE_EXTENSION/)) { 10 | return { exit: t.end.bind(t) }; 11 | } 12 | }); 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /test/spawn/typescript.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn, touchFile } = require('../utils'); 4 | 5 | tap.test('Uses ts-node/register for .ts files through config file (also the default)', t => { 6 | spawn('typescript/index.ts', out => { 7 | if (out.match(/touch message.ts/)) { 8 | touchFile('typescript', 'message.ts'); 9 | return out2 => { 10 | if (out2.match(/Restarting/)) { 11 | t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); 12 | return { exit: t.end.bind(t) }; 13 | } 14 | }; 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/spawn/uncaught.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | 3 | const { spawn } = require('../utils'); 4 | 5 | tap.test('should run async code uncaughtException handlers', t => { 6 | spawn('uncaught-exception-handler.js', out => { 7 | if (out.match(/ERROR/)) { 8 | return out2 => { 9 | if (out2.match(/async \[?ReferenceError/)) { 10 | return { exit: t.end.bind(t) }; 11 | } 12 | }; 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | const { join } = require('path'); 3 | const touch = require('touch'); 4 | 5 | const { control } = require('../lib/clear'); 6 | 7 | const bin = join(__dirname, '..', 'bin', 'node-dev'); 8 | const dir = join(__dirname, 'fixture'); 9 | 10 | const reClear = new RegExp(control); 11 | 12 | const noop = () => {/**/}; 13 | 14 | exports.spawn = (cmd, cb = noop) => { 15 | const ps = spawn('node', [bin].concat(cmd.split(' ')), { cwd: dir }); 16 | let err = ''; 17 | 18 | function errorHandler(data) { 19 | err += data.toString(); 20 | outHandler(data); 21 | } 22 | 23 | function exitHandler(code, signal) { 24 | if (err) cb(err, code, signal); 25 | } 26 | 27 | function outHandler(data) { 28 | // Don't log clear 29 | console.log(data.toString().replace(reClear, '')); 30 | 31 | const ret = cb.call(ps, data.toString()); 32 | 33 | if (typeof ret === 'function') { 34 | // use the returned function as new callback 35 | cb = ret; 36 | } else if (ret && ret.exit) { 37 | // kill the process and invoke the given function 38 | ps.removeListener('exit', exitHandler); 39 | ps.once('exit', code => { 40 | ps.stdout.removeListener('data', outHandler); 41 | ps.stderr.removeListener('data', errorHandler); 42 | ret.exit(code); 43 | }); 44 | ps.kill('SIGTERM'); 45 | } 46 | } 47 | 48 | ps.stderr.on('data', errorHandler); 49 | ps.once('exit', exitHandler); 50 | ps.stdout.on('data', outHandler); 51 | 52 | return ps; 53 | }; 54 | 55 | // filewatcher requires a new mtime to trigger a change event 56 | // but most file systems only have second precision, so wait 57 | // one full second before touching. 58 | 59 | exports.touchFile = (...filepath) => { 60 | setTimeout(() => touch(join(dir, ...filepath)), 1000); 61 | }; 62 | --------------------------------------------------------------------------------