Repository: fgnass/node-dev (js)

Issue:
notify option no longer works when set in .node-dev.json
I think there should be a "not undefined" condition here so that it does not get overwritten by the CLI options:
https://github.com/fgnass/node-dev/commit/e17baec4c111e7ee31653104d5bb3c10b344b65e#diff-48207702c14d7936c39cbc1317b1e3a5R30

Pull request:
v5.0.0 / 2020-07-08
- Remove `--all-deps` and `--no-deps` CLI options, use `--deps=-1` or `--deps=0` respectively
- Unify `cli` and `cfg` logic to ensure CLI always overrides config files
- Load order for config files now matches what is in the `README`
- Add tests for notify, CLI should override config files
- All config now have clear default values
- Use more ES6 code
- Rename `resolveMain.js` to `resolve-main.js`

I am making this a major bump because CLI options have been removed and enough code has changed that I'm not 100% confident I didn't break something.

Fixes #229 

Patch:
diff --git a/.eslintrc b/.eslintrc
index fff6274..dbf2b66 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -5,6 +5,9 @@
   "extends": [
     "airbnb-base/legacy"
   ],
+  "parserOptions": {
+    "ecmaVersion": 9
+  },
   "rules": {
     "comma-dangle": [2, "never"],
     "consistent-return": 0,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f35f02..51fdd72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # node-dev
 
+## v5.0.0 / 2020-07-08
+
+- Remove `--all-deps` and `--no-deps` CLI options, use `--deps=-1` or `--deps=0` respectively
+- Unify `cli` and `cfg` logic to ensure CLI always overrides config files
+- Load order for config files now matches what is in the `README`
+- Add tests for notify, CLI should override config files
+- All config now have clear default values
+- Use more ES6 code
+- Rename `resolveMain.js` to `resolve-main.js`
+
 ## v4.3.0 / 2020-07-03
 
 - Enable `--notify` by default and add tests
diff --git a/README.md b/README.md
index ea399fe..e0193e0 100644
--- a/README.md
+++ b/README.md
@@ -35,12 +35,20 @@ node-dev foo.js
 There are a couple of command line options that can be used to control which
 files are watched and what happens when they change:
 
-* `--no-deps` Watch only the project's own files and linked modules (via `npm link`)
-* `--all-deps` Watch the whole dependency tree
-* `--respawn` Keep watching for changes after the script has exited
-* `--dedupe` [Dedupe dynamically](https://www.npmjs.org/package/dynamic-dedupe)
-* `--graceful_ipc <msg>` Send 'msg' as an IPC message instead of SIGTERM for restart/shutdown
-* `--poll` Force polling for file changes (Caution! CPU-heavy!)
+* `--clear` - Clear the screen on restart
+* `--dedupe` - [Dedupe dynamically](https://www.npmjs.org/package/dynamic-dedupe)
+* `--deps`:
+  * -1 - Watch the whole dependency tree
+  * 0 - Watch only the project's own files and linked modules (via `npm link`)
+  * 1 (_Default_) - Watch all first level dependencies
+* `--fork` - Hook into child_process.fork
+* `--graceful_ipc <msg>` - Send 'msg' as an IPC message instead of SIGTERM for restart/shutdown
+* `--ignore` - A file whose changes should not cause a restart
+* `--notify` - Display desktop notifications
+* `--poll` - Force polling for file changes (Caution! CPU-heavy!)
+* `--respawn` - Keep watching for changes after the script has exited
+* `--timestamp` - The timestamp format to use for logging restarts
+* `--vm` - Load files using Node's VM
 
 By default node-dev will watch all first-level dependencies, i.e. the ones in
 the project's `node_modules`folder.
@@ -76,13 +84,16 @@ Usually node-dev doesn't require any configuration at all, but there are some
 options you can set to tweak its behaviour:
 
 * `clear` – Whether to clear the screen upon restarts. _Default:_ `false`
+* `dedupe` – Whether modules should by [dynamically deduped](https://www.npmjs.org/package/dynamic-dedupe). _Default:_ `false`
+* `deps` – How many levels of dependencies should be watched. _Default:_ `1`
+* `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`
+* `graceful_ipc` - Send the argument provided as an IPC message instead of SIGTERM during restart events. _Default:_ `""` (off)
+* `ignore` - A single file or an array of files to ignore. _Default:_ `[]`
 * `notify` – Whether to display desktop notifications. _Default:_ `true`
+* `poll` - Force polling for file changes, this can be CPU-heavy. _Default:_ `false`
+* `respawn` - Keep watching for changes after the script has exited. _Default:_ `false`
 * `timestamp` – The timestamp format to use for logging restarts. _Default:_ `"HH:MM:ss"`
 * `vm` – Whether to watch files loaded via Node's [VM](http://nodejs.org/docs/latest/api/vm.html) module. _Default:_ `true`
-* `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`
-* `deps` – How many levels of dependencies should be watched. _Default:_ `1`
-* `dedupe` – Whether modules should by [dynamically deduped](https://www.npmjs.org/package/dynamic-dedupe). _Default:_ `false`
-* `graceful_ipc` - Send the argument provided as an IPC message instead of SIGTERM during restart events. _Default:_ `""` (off)
 
 Upon startup node-dev looks for a `.node-dev.json` file in the following directories:
 * user's HOME directory
diff --git a/lib/cfg.js b/lib/cfg.js
index ecedc3b..1f5e158 100644
--- a/lib/cfg.js
+++ b/lib/cfg.js
@@ -1,51 +1,44 @@
-var fs = require('fs');
-var path = require('path');
+const fs = require('fs');
+const path = require('path');
+
+const resolveMain = require('./resolve-main');
+
+const defaultConfig = {
+  clear: false,
+  dedupe: false,
+  deps: 1,
+  extensions: {
+    coffee: 'coffeescript/register',
+    ls: 'LiveScript'
+  },
+  fork: true,
+  graceful_ipc: '',
+  ignore: [],
+  notify: true,
+  poll: false,
+  respawn: false,
+  timestamp: 'HH:MM:ss',
+  vm: true
+};
 
 function read(dir) {
-  var f = path.resolve(dir, '.node-dev.json');
+  const f = path.resolve(dir, '.node-dev.json');
   return fs.existsSync(f) ? JSON.parse(fs.readFileSync(f)) : {};
 }
 
-function resolvePath(unresolvedPath) {
-  return path.resolve(process.cwd(), unresolvedPath);
-}
-
-module.exports = function (main, opts) {
-  var dir = main ? path.dirname(main) : '.';
-  var c = Object.assign(read(process.cwd()), read(dir));
-
-  /* eslint-disable no-proto */
-  c.__proto__ = read(process.env.HOME || process.env.USERPROFILE);
+function getConfig(script) {
+  const main = resolveMain(script);
+  const dir = main ? path.dirname(main) : '.';
 
-  // Truthy == --all-deps, false: one level of deps
-  if (typeof c.deps !== 'number') c.deps = c.deps ? -1 : 1;
-
-  if (opts) {
-    // Overwrite with CLI opts ...
-    if (opts.allDeps) c.deps = -1;
-    if (!opts.deps) c.deps = 0;
-    if (opts.dedupe) c.dedupe = true;
-    if (opts.graceful_ipc) c.graceful_ipc = opts.graceful_ipc;
-    if (opts.respawn) c.respawn = true;
-    c.notify = opts.notify;
-  }
-
-  var ignore = (c.ignore || []).map(resolvePath);
+  return Object.assign(
+    defaultConfig,
+    read(process.env.HOME || process.env.USERPROFILE),
+    read(process.cwd()),
+    read(dir)
+  );
+}
 
-  return {
-    vm: c.vm !== false,
-    fork: c.fork !== false,
-    notify: c.notify,
-    deps: c.deps,
-    timestamp: c.timestamp || (c.timestamp !== false && 'HH:MM:ss'),
-    clear: !!c.clear,
-    dedupe: !!c.dedupe,
-    graceful_ipc: c.graceful_ipc,
-    ignore: ignore,
-    respawn: c.respawn || false,
-    extensions: c.extensions || {
-      coffee: 'coffeescript/register',
-      ls: 'LiveScript'
-    }
-  };
+module.exports = {
+  defaultConfig,
+  getConfig
 };
diff --git a/lib/cli.js b/lib/cli.js
index 907fabe..83d021c 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -1,15 +1,26 @@
 const minimist = require('minimist');
+const path = require('path');
+
+const { defaultConfig, getConfig } = require('./cfg');
+
+const configKeys = Object.keys(defaultConfig);
+
+function resolvePath(unresolvedPath) {
+  return path.resolve(process.cwd(), unresolvedPath);
+}
 
 function getFirstNonOptionArgIndex(args) {
   for (let i = 2; i < args.length; i += 1) {
     if (args[i][0] != '-') return i;
   }
+
   return args.length;
 }
 
 function removeValueArgs(args, names) {
   let i = 0;
   let removed = [];
+
   while (i < args.length) {
     if (names.includes(args[i])) {
       removed = removed.concat(args.splice(i, 2));
@@ -17,6 +28,7 @@ function removeValueArgs(args, names) {
       i += 1;
     }
   }
+
   return removed;
 }
 
@@ -29,14 +41,22 @@ module.exports = function (argv) {
   const devArgs = argv.slice(2, scriptIndex);
 
   const opts = minimist(devArgs, {
-    boolean: ['all-deps', 'deps', 'dedupe', 'poll', 'respawn', 'notify'],
-    string: ['graceful_ipc'],
-    default: { deps: true, notify: true, graceful_ipc: '' },
+    boolean: ['clear', 'dedupe', 'fork', 'notify', 'poll', 'respawn', 'vm'],
+    string: ['graceful_ipc', 'ignore', 'timestamp'],
+    default: getConfig(script),
     unknown: function (arg) {
+      const argKeys = Object.keys(minimist([arg]));
+
+      if (configKeys.some(k => argKeys.includes(k))) {
+        return true;
+      }
+
       nodeArgs.push(arg);
     }
   });
 
+  opts.ignore = [...Array.isArray(opts.ignore) ? opts.ignore : [opts.ignore]].map(resolvePath);
+
   return {
     script,
     scriptArgs,
diff --git a/lib/hook.js b/lib/hook.js
index f84d9c3..86c1799 100644
--- a/lib/hook.js
+++ b/lib/hook.js
@@ -1,12 +1,11 @@
-var vm = require('vm');
-
-module.exports = function (cfg, wrapper, callback) {
+const vm = require('vm');
 
+module.exports = function (patchVM, wrapper, callback) {
   // Hook into Node's `require(...)`
   updateHooks();
 
   // Patch the vm module to watch files executed via one of these methods:
-  if (cfg.vm) {
+  if (patchVM) {
     patch(vm, 'createScript', 1);
     patch(vm, 'runInThisContext', 1);
     patch(vm, 'runInNewContext', 2);
@@ -18,11 +17,11 @@ module.exports = function (cfg, wrapper, callback) {
    * index.
    */
   function patch(obj, method, optionsArgIndex) {
-    var orig = obj[method];
+    const orig = obj[method];
     if (!orig) return;
     obj[method] = function () {
-      var opts = arguments[optionsArgIndex];
-      var file = null;
+      const opts = arguments[optionsArgIndex];
+      let file = null;
       if (opts) {
         file = typeof opts == 'string' ? opts : opts.filename;
       }
@@ -36,7 +35,7 @@ module.exports = function (cfg, wrapper, callback) {
    */
   function updateHooks() {
     Object.keys(require.extensions).forEach(function (ext) {
-      var fn = require.extensions[ext];
+      const fn = require.extensions[ext];
       if (typeof fn === 'function' && fn.name !== 'nodeDevHook') {
         require.extensions[ext] = createHook(fn);
       }
diff --git a/lib/index.js b/lib/index.js
index 9a4c852..067668e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,10 +1,23 @@
-var fork = require('child_process').fork;
-var filewatcher = require('filewatcher');
-var path = require('path');
-var ipc = require('./ipc');
-var resolveMain = require('./resolveMain');
-
-module.exports = function (script, scriptArgs, nodeArgs, opts) {
+const fork = require('child_process').fork;
+const filewatcher = require('filewatcher');
+const path = require('path');
+
+const ipc = require('./ipc');
+const logFactory = require('./log');
+const notifyFactory = require('./notify');
+const resolveMain = require('./resolve-main');
+
+module.exports = function (script, scriptArgs, nodeArgs, {
+  clear,
+  dedupe,
+  deps,
+  graceful_ipc: gracefulIPC,
+  ignore,
+  notify: notifyEnabled,
+  poll: forcePolling,
+  respawn,
+  timestamp
+}) {
   if (!script) {
     console.log('Usage: node-dev [options] script [arguments]\n');
     process.exit(1);
@@ -22,23 +35,22 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
     throw new TypeError('`nodeArgs` must be an array');
   }
 
+  const log = logFactory({ timestamp });
+  const notify = notifyFactory(notifyEnabled, log);
+
   // The child_process
-  var child;
+  let child;
 
-  var wrapper = resolveMain(path.join(__dirname, 'wrap.js'));
-  var main = resolveMain(script);
-  var cfg = require('./cfg')(main, opts);
-  var log = require('./log')(cfg);
-  var notify = require('./notify')(cfg, log);
+  const wrapper = resolveMain(path.join(__dirname, 'wrap.js'));
 
   // Run ./dedupe.js as preload script
-  if (cfg.dedupe) process.env.NODE_DEV_PRELOAD = path.join(__dirname, 'dedupe');
+  if (dedupe) process.env.NODE_DEV_PRELOAD = path.join(__dirname, 'dedupe');
 
-  var watcher = filewatcher({ forcePolling: opts.poll });
+  const watcher = filewatcher({ forcePolling });
 
   watcher.on('change', function (file) {
     /* eslint-disable no-octal-escape */
-    if (cfg.clear) process.stdout.write('\033[2J\033[H');
+    if (clear) process.stdout.write('\033[2J\033[H');
     notify('Restarting', file + ' has been modified');
     watcher.removeAll();
     if (child) {
@@ -55,20 +67,20 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
     log.warn('node-dev ran out of file handles after watching %s files.', limit);
     log.warn('Falling back to polling which uses more CPU.');
     log.info('Run ulimit -n 10000 to increase the file descriptor limit.');
-    if (cfg.deps) log.info('... or add `--no-deps` to use less file handles.');
+    if (deps) log.info('... or add `--deps=0` to use fewer file handles.');
   });
 
   /**
    * Run the wrapped script.
    */
   function start() {
-    var cmd = nodeArgs.concat(wrapper, script, scriptArgs);
+    const cmd = nodeArgs.concat(wrapper, script, scriptArgs);
     child = fork(cmd[0], cmd.slice(1), {
       cwd: process.cwd(),
       env: process.env
     });
 
-    if (cfg.respawn) {
+    if (respawn) {
       child.respawn = true;
     }
     child.on('exit', function (code) {
@@ -78,9 +90,9 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
 
     // Listen for `required` messages and watch the required file.
     ipc.on(child, 'required', function (m) {
-      var isIgnored = cfg.ignore.some(isPrefixOf(m.required));
+      const isIgnored = ignore.some(isPrefixOf(m.required));
 
-      if (!isIgnored && (cfg.deps === -1 || getLevel(m.required) <= cfg.deps)) {
+      if (!isIgnored && (deps === -1 || getLevel(m.required) <= deps)) {
         watcher.add(m.required);
       }
     });
@@ -95,9 +107,9 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
   function stop(willTerminate) {
     child.respawn = true;
     if (!willTerminate) {
-      if (cfg.graceful_ipc) {
-        log.info('Sending IPC: ' + JSON.stringify(cfg.graceful_ipc));
-        child.send(cfg.graceful_ipc);
+      if (gracefulIPC) {
+        log.info('Sending IPC: ' + JSON.stringify(gracefulIPC));
+        child.send(gracefulIPC);
       } else {
         child.kill('SIGTERM');
       }
@@ -108,9 +120,9 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
   // Relay SIGTERM
   process.on('SIGTERM', function () {
     if (child && child.connected) {
-      if (cfg.graceful_ipc) {
-        log.info('Sending IPC: ' + JSON.stringify(cfg.graceful_ipc));
-        child.send(cfg.graceful_ipc);
+      if (gracefulIPC) {
+        log.info('Sending IPC: ' + JSON.stringify(gracefulIPC));
+        child.send(gracefulIPC);
       } else {
         child.kill('SIGTERM');
       }
@@ -128,7 +140,7 @@ module.exports = function (script, scriptArgs, nodeArgs, opts) {
  * a positive integer otherwise.
  */
 function getLevel(mod) {
-  var p = getPrefix(mod);
+  const p = getPrefix(mod);
   return p.split('node_modules').length - 1;
 }
 
@@ -137,8 +149,8 @@ function getLevel(mod) {
  * empty string if the path does not contain a node_modules dir.
  */
 function getPrefix(mod) {
-  var n = 'node_modules';
-  var i = mod.lastIndexOf(n);
+  const n = 'node_modules';
+  const i = mod.lastIndexOf(n);
   return i !== -1 ? mod.slice(0, i + n.length) : '';
 }
 
diff --git a/lib/log.js b/lib/log.js
index 8245c36..6a5a02c 100644
--- a/lib/log.js
+++ b/lib/log.js
@@ -19,14 +19,14 @@ function colorFactory(enableColor) {
  * Logs a message to the console. The level is displayed in ANSI colors,
  * either bright red in case of an error or green otherwise.
  */
-module.exports = function (cfg) {
-  var enableColor = !(cfg.noColor || !process.stdout.isTTY);
+module.exports = function ({ noColor, timestamp }) {
+  var enableColor = !(noColor || !process.stdout.isTTY);
   var color = colorFactory(enableColor);
 
   function log(msg, level) {
-    var timestamp = cfg.timestamp ? color(fmt(new Date(), cfg.timestamp), '39') + ' ' : '';
+    var ts = timestamp ? color(fmt(new Date(), timestamp), '39') + ' ' : '';
     var c = colors[level.toLowerCase()] || '32';
-    var output = '[' + color(level.toUpperCase(), c) + '] ' + timestamp + msg;
+    var output = '[' + color(level.toUpperCase(), c) + '] ' + ts + msg;
     console.log(output);
     return output;
   }
diff --git a/lib/notify.js b/lib/notify.js
index ef25a64..40fd164 100644
--- a/lib/notify.js
+++ b/lib/notify.js
@@ -8,11 +8,11 @@ function icon(level) {
 /**
  * Displays a desktop notification and writes a message to the console.
  */
-module.exports = function (cfg, log) {
+module.exports = function (notifyEnabled, log) {
   return function (title, msg, level) {
     level = level || 'info';
     log(title || msg, level);
-    if (cfg.notify) {
+    if (notifyEnabled) {
       notifier.notify({
         title: title || 'node.js',
         icon: icon(level),
diff --git a/lib/resolveMain.js b/lib/resolve-main.js
similarity index 100%
rename from lib/resolveMain.js
rename to lib/resolve-main.js
diff --git a/lib/wrap.js b/lib/wrap.js
index e54c7a0..274eeca 100755
--- a/lib/wrap.js
+++ b/lib/wrap.js
@@ -1,18 +1,17 @@
-var path = require('path');
-var childProcess = require('child_process');
-var fork = childProcess.fork;
-var resolve = require('resolve').sync;
-var hook = require('./hook');
-var ipc = require('./ipc');
-var resolveMain = require('./resolveMain');
+const path = require('path');
+const childProcess = require('child_process');
+const resolve = require('resolve').sync;
+
+const { getConfig } = require('./cfg');
+const hook = require('./hook');
+const ipc = require('./ipc');
+const resolveMain = require('./resolve-main');
 
 // Remove wrap.js from the argv array
 process.argv.splice(1, 1);
 
-// Resolve the location of the main script relative to cwd
-var main = resolveMain(process.argv[1]);
-
-var cfg = require('./cfg')(main);
+const script = process.argv[1];
+const { extensions, fork, vm } = getConfig(script);
 
 if (process.env.NODE_DEV_PRELOAD) {
   require(process.env.NODE_DEV_PRELOAD);
@@ -23,11 +22,12 @@ process.on('SIGTERM', function () {
   if (process.listeners('SIGTERM').length === 1) process.exit(0);
 });
 
-if (cfg.fork) {
+if (fork) {
   // Overwrite child_process.fork() so that we can hook into forked processes
   // too. We also need to relay messages about required files to the parent.
+  const originalFork = childProcess.fork;
   childProcess.fork = function (modulePath, args, options) {
-    var child = fork(__filename, [modulePath].concat(args), options);
+    const child = originalFork(__filename, [modulePath].concat(args), options);
     ipc.relay(child);
     return child;
   };
@@ -38,7 +38,7 @@ process.on('uncaughtException', function (err) {
   console.error(err.stack || err);
   // If there's a custom uncaughtException handler expect it to terminate
   // the process.
-  var hasCustomHandler = process.listeners('uncaughtException').length > 1;
+  const hasCustomHandler = process.listeners('uncaughtException').length > 1;
   ipc.send({
     error: err.name || 'Error',
     message: err.message,
@@ -47,23 +47,25 @@ process.on('uncaughtException', function (err) {
 });
 
 // Hook into require() and notify the parent process about required files
-hook(cfg, module, function (file) {
-  ipc.send({ required: file });
+hook(vm, module, function (required) {
+  ipc.send({ required });
 });
 
 // Check if a module is registered for this extension
-var ext = path.extname(main).slice(1);
-var mod = cfg.extensions[ext];
+const main = resolveMain(script);
+const ext = path.extname(main).slice(1);
+const mod = extensions[ext];
+const basedir = path.dirname(main);
 
 // Support extensions where 'require' returns a function that accepts options
 if (typeof mod == 'object' && mod.name) {
-  var fn = require(resolve(mod.name, { basedir: path.dirname(main) }));
+  const fn = require(resolve(mod.name, { basedir }));
   if (typeof fn == 'function' && mod.options) {
     // require returned a function, call it with options
     fn(mod.options);
   }
 } else if (typeof mod == 'string') {
-  require(resolve(mod, { basedir: path.dirname(main) }));
+  require(resolve(mod, { basedir }));
 }
 
 // Execute the wrapped script
diff --git a/package.json b/package.json
index fac5854..7cd47d0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "node-dev",
-  "version": "4.3.0",
+  "version": "5.0.0",
   "description": "Restarts your app when files are modified",
   "keywords": [
     "restart",


Test patch:
diff --git a/test/cli.js b/test/cli.js
index 4160ec1..fbe319c 100644
--- a/test/cli.js
+++ b/test/cli.js
@@ -3,15 +3,50 @@ const tap = require('tap');
 const cli = require('../lib/cli.js');
 
 tap.test('notify is enabled by default', t => {
-  const { opts: { notify } } = cli([]);
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', 'test']);
 
   t.is(notify, true);
   t.done();
 });
 
-tap.test('notify can be disabled', t => {
-  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--notify=false', 'foo.js']);
+tap.test('--no-notify', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--no-notify', 'test']);
 
   t.is(notify, false);
   t.done();
 });
+
+tap.test('--notify=false', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--notify=false', 'test']);
+
+  t.is(notify, false);
+  t.done();
+});
+
+tap.test('--notify', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--notify', 'test']);
+
+  t.is(notify, true);
+  t.done();
+});
+
+tap.test('--notify=true', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--notify=true', 'test']);
+
+  t.is(notify, true);
+  t.done();
+});
+
+tap.test('notify can be disabled by .node-dev.json', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', 'test/fixture/server.js']);
+
+  t.is(notify, false);
+  t.done();
+});
+
+tap.test('cli overrides .node-dev.json from false to true', t => {
+  const { opts: { notify } } = cli(['node', 'bin/node-dev', '--notify=true', 'test/fixture/server.js']);
+
+  t.is(notify, true);
+  t.done();
+});
diff --git a/test/log.js b/test/log.js
index f96c324..3492a14 100644
--- a/test/log.js
+++ b/test/log.js
@@ -1,34 +1,36 @@
-var tap = require('tap');
+const tap = require('tap');
 
-var cfg = require('../lib/cfg.js')();
+const { defaultConfig } = require('../lib/cfg');
+const logFactory = require('../lib/log');
 
-cfg.noColor = true;
-
-var logFactory = require('../lib/log.js');
-var log = logFactory(cfg);
+const noColorCfg = { ...defaultConfig, noColor: true };
 
 tap.test('log.info', function (t) {
-  var out = log.info('hello');
-  t.like(out, /\[INFO\] \d{2}:\d{2}:\d{2} hello/);
+  const log = logFactory(noColorCfg);
+  t.like(log.info('hello'), /\[INFO\] \d{2}:\d{2}:\d{2} hello/);
   t.done();
 });
 
 tap.test('log.warn', function (t) {
-  var out = log.warn('a warning');
-  t.like(out, /\[WARN\] \d{2}:\d{2}:\d{2} a warning/);
+  const log = logFactory(noColorCfg);
+  t.like(log.warn('a warning'), /\[WARN\] \d{2}:\d{2}:\d{2} a warning/);
   t.done();
 });
 
 tap.test('log.error', function (t) {
-  var out = log.error('an error');
-  t.like(out, /\[ERROR\] \d{2}:\d{2}:\d{2} an error/);
+  const log = logFactory(noColorCfg);
+  t.like(log.error('an error'), /\[ERROR\] \d{2}:\d{2}:\d{2} an error/);
+  t.done();
+});
+
+tap.test('Disable the timestamp', function (t) {
+  const log = logFactory({ ...noColorCfg, timestamp: false });
+  t.like(log.info('no timestamp'), /\[INFO\] no timestamp/);
   t.done();
 });
 
-tap.test('Disable the timestmap', function (t) {
-  var noTsCfg = Object.assign({}, cfg, { timestamp: false });
-  var noTsLog = logFactory(noTsCfg);
-  var out = noTsLog.info('no timestamp');
-  t.like(out, /\[INFO\] no timestamp/);
+tap.test('Custom timestamp', function (t) {
+  const log = logFactory({ ...noColorCfg, timestamp: 'yyyy-mm-dd HH:MM:ss' });
+  t.like(log.error('an error'), /\[ERROR\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} an error/);
   t.done();
 });