From 18d5b17f10f966c24cfed9ee6cb7a54409b79cb6 Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Sat, 14 Sep 2019 12:59:47 -0700 Subject: [PATCH 1/6] ignore .idea for those who work with that --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 720a2b6..a895aff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vagrant node_modules *.log -.ts-node \ No newline at end of file +.ts-node +.idea From 1c36b43e92bad7b3e44d8032dbc380959be0bf51 Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Sat, 14 Sep 2019 13:00:56 -0700 Subject: [PATCH 2/6] support config notifyLevel --- bin/ts-node-dev | 1 + lib/cfg.js | 2 ++ lib/notify.js | 11 ++++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/ts-node-dev b/bin/ts-node-dev index 69f8143..5c76b54 100644 --- a/bin/ts-node-dev +++ b/bin/ts-node-dev @@ -51,6 +51,7 @@ var opts = minimist(devArgs, { 'ignore-watch', 'interval', 'debounce', + 'notifyLevel', 'watch' ], alias: { diff --git a/lib/cfg.js b/lib/cfg.js index c6a5e9e..efac5f2 100644 --- a/lib/cfg.js +++ b/lib/cfg.js @@ -28,6 +28,7 @@ module.exports = function (main, opts) { if (opts.respawn) c.respawn = true; if (opts.notify === false) c.notify = false; if (opts.clear || opts.cls) c.clear = true; + if (opts.notifyLevel) c.notifyLevel = opts.notifyLevel } var ignoreWatch = ([]).concat(opts && opts['ignore-watch'] || []).concat(c.ignore || []); @@ -37,6 +38,7 @@ module.exports = function (main, opts) { vm: c.vm !== false, fork: c.fork !== false, notify: c.notify !== false, + notifyLevel: c.notifyLevel || 'DEBUG', deps: c.deps, timestamp: c.timestamp || (c.timestamp !== false && 'HH:MM:ss'), clear: !!(c.clear), diff --git a/lib/notify.js b/lib/notify.js index 204ba71..efd2c42 100644 --- a/lib/notify.js +++ b/lib/notify.js @@ -5,6 +5,15 @@ function icon(level) { return path.resolve(__dirname, '../icons/node_' + level + '.png'); } +function levelToInt(strLevel) { + if (!strLevel) return 0; + switch (strLevel.toLowerCase()) { + case 'info': return 1; + case 'error': return 3; + default: return 0; + } +} + /** * Displays a desktop notification and writes a message to the console. */ @@ -12,7 +21,7 @@ module.exports = function (cfg, log) { return function (title, msg, level) { level = level || 'info'; log([title, msg].filter(_ => _).join(': '), level); - if (cfg.notify) { + if (cfg.notify && levelToInt(cfg.notifyLevel) <= levelToInt(level)) { notifier.notify({ title: title || 'node.js', icon: icon(level), From 4e847c92fe56e911adc124f8c0f162aa56a0999c Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Sat, 14 Sep 2019 13:01:43 -0700 Subject: [PATCH 3/6] notify on uncaught process errors --- lib/child-require-hook.js | 8 ++++++++ lib/compiler.js | 8 ++++++++ lib/index.js | 1 + lib/notify.js | 4 +++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/child-require-hook.js b/lib/child-require-hook.js index 35989c8..409ed06 100644 --- a/lib/child-require-hook.js +++ b/lib/child-require-hook.js @@ -2,6 +2,7 @@ var fs = require('fs') var getCompiledPath = require('./get-compiled-path') var sep = require('path').sep var join = require('path').join +var notify = require(join(__dirname, './notify')) var execSync = require('child_process').execSync var compilationId var timeThreshold = 0 @@ -12,6 +13,7 @@ var ignore = [/node_modules/] var readyFile var execCheck = false var sourceMapSupportPath +var cfg = {} var checkFileScript = join(__dirname, 'check-file-exists.js') @@ -112,6 +114,12 @@ if (readyFile) { } } +process.on('uncaughtException', err => { + notify(cfg, null, err)('Error', err.message || err.stack, 'error') + process.exitCode = 1 + process.kill(process.pid, 'SIGTERM') +}) + process.on('SIGTERM', function() { console.log('Child got SIGTERM, exiting.') process.exit() diff --git a/lib/compiler.js b/lib/compiler.js index e288dc2..2afdbe5 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -24,6 +24,10 @@ var compilationInstanceStamp = Math.random() var compiler = { allowJs: false, tsConfigPath: '', + cfg: {}, + setConfig(cfg) { + this.cfg = cfg + }, getCompilationId: function() { return compilationInstanceStamp }, @@ -113,6 +117,10 @@ var compiler = { /__dirname/, '"' + __dirname.replace(/\\/g, '/') + '"' ) + fileData = fileData.replace( + 'var cfg = {}', + 'var cfg = ' + JSON.stringify(this.cfg) + ) fs.writeFileSync(compiler.getChildHookPath(), fileData) }, init: function(options) { diff --git a/lib/index.js b/lib/index.js index d32ef75..463cc0c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,6 +30,7 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { var log = require('./log')(cfg) var notify = require('./notify')(cfg, log) opts.log = log + compiler.setConfig(cfg) compiler.init(opts) compiler.notify = notify diff --git a/lib/notify.js b/lib/notify.js index efd2c42..ccc2ab7 100644 --- a/lib/notify.js +++ b/lib/notify.js @@ -20,7 +20,9 @@ function levelToInt(strLevel) { module.exports = function (cfg, log) { return function (title, msg, level) { level = level || 'info'; - log([title, msg].filter(_ => _).join(': '), level); + if (log) { + log([title, msg].filter(_ => _).join(': '), level); + } if (cfg.notify && levelToInt(cfg.notifyLevel) <= levelToInt(level)) { notifier.notify({ title: title || 'node.js', From 051beb55ada7b4efa594ce02317c6f2df667dd3c Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Sat, 14 Sep 2019 13:01:50 -0700 Subject: [PATCH 4/6] kill process with SIGINIT --- lib/child-require-hook.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/child-require-hook.js b/lib/child-require-hook.js index 409ed06..043755a 100644 --- a/lib/child-require-hook.js +++ b/lib/child-require-hook.js @@ -121,8 +121,9 @@ process.on('uncaughtException', err => { }) process.on('SIGTERM', function() { + // This is to make sure debuggers (e.g. Chrome) close console.log('Child got SIGTERM, exiting.') - process.exit() + process.kill(process.pid, 'SIGINT') }) module.exports.registerExtensions = registerExtensions From 948167a9a07965ffd8b9e16ff60130368fe46f0d Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Sat, 14 Sep 2019 13:05:21 -0700 Subject: [PATCH 5/6] docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 08e852b..8f1175d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ tsnd --respawn server.ts - `--debounce` - Debounce file change events (ms, non-polling mode) - `--clear` (`--cls`) Will clear screen on restart - `--watch` - Explicitly add files or folders to watch and restart on change (list separated by commas) +- `--notifyLevel` - `error` or `info` (default). The level for which OS level notifications should be sent **Caveats and points of notice:** From f1f2c8427ad841681bfdee02b508d90d45d15e99 Mon Sep 17 00:00:00 2001 From: Staffan Eketorp Date: Mon, 16 Sep 2019 14:12:24 -0700 Subject: [PATCH 6/6] also watching files that don't currently exist --- lib/cfg.js | 2 +- lib/index.js | 19 ++++++++++++++++--- lib/wrap.js | 19 ++++++++++++++++--- package.json | 3 ++- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/cfg.js b/lib/cfg.js index efac5f2..4d53d75 100644 --- a/lib/cfg.js +++ b/lib/cfg.js @@ -28,7 +28,7 @@ module.exports = function (main, opts) { if (opts.respawn) c.respawn = true; if (opts.notify === false) c.notify = false; if (opts.clear || opts.cls) c.clear = true; - if (opts.notifyLevel) c.notifyLevel = opts.notifyLevel + if (opts.notifyLevel) c.notifyLevel = opts.notifyLevel; } var ignoreWatch = ([]).concat(opts && opts['ignore-watch'] || []).concat(c.ignore || []); diff --git a/lib/index.js b/lib/index.js index 463cc0c..e9dea11 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,6 +7,8 @@ var fs = require('fs') var tsNodeVersion = require('ts-node').VERSION var tsVersion = require('typescript').version var kill = require('tree-kill') +var path = require('path') +var chokidar = require('chokidar') module.exports = function(script, scriptArgs, nodeArgs, opts) { if (typeof script !== 'string' || script.length === 0) { @@ -38,6 +40,7 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { // Run ./dedupe.js as preload script if (cfg.dedupe) process.env.NODE_DEV_PRELOAD = __dirname + '/dedupe' + var chokidarWatches = [] var watcher = filewatcher({ forcePolling: opts.poll, interval: parseInt(opts.interval), @@ -45,7 +48,7 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { recursive: true }) var starting = false - watcher.on('change', function(file) { + function onFileChanged(file) { if (file === compiler.tsConfigPath) { notify('Reinitializing TS compilation') compiler.init(opts) @@ -62,6 +65,8 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { return } log.debug('Removing all watchers from files') + chokidarWatches.forEach(nw => nw.close()) + chokidarWatches = [] watcher.removeAll() starting = true if (child) { @@ -72,7 +77,8 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { log.debug('Child is already stopped, probably due to a previous error') start() } - }) + } + watcher.on('change', onFileChanged) watcher.on('fallback', function(limit) { log.warn('node-dev ran out of file handles after watching %s files.', limit) @@ -162,7 +168,14 @@ module.exports = function(script, scriptArgs, nodeArgs, opts) { // Upon errors, display a notification and tell the child to exit. ipc.on(child, 'error', function(m) { - log.debug('Child error') + log.debug('Child error', m) + var lastRequire = m.lastRequire + if (m.code === 'MODULE_NOT_FOUND' && lastRequire && lastRequire.path.startsWith('.')) { + var pathNoExt = path.normalize(path.join(path.dirname(lastRequire.filename), lastRequire.path)) + var watch = chokidar.watch([pathNoExt + '.ts', pathNoExt + '.tsx']) + watch.on('all', (arg, file) => onFileChanged(file)) + chokidarWatches.push(watch) + } notify(m.error, m.message, 'error') stop(m.willTerminate) }) diff --git a/lib/wrap.js b/lib/wrap.js index 486e40f..988efd2 100755 --- a/lib/wrap.js +++ b/lib/wrap.js @@ -1,3 +1,4 @@ +var Module = require('module') var path = require('path'); var childProcess = require('child_process'); var fork = childProcess.fork; @@ -21,6 +22,15 @@ if (process.env.NODE_DEV_PRELOAD) { require(process.env.NODE_DEV_PRELOAD); } +// We hook on the require call to keep track of the last required files in case there's a +// MODULE_NOT_FOUND error for file watching +var lastRequire = null; +var origRequire = Module.prototype.require; +Module.prototype.require = function (requirePath) { + lastRequire = { path: requirePath, filename: this.filename }; + return origRequire.apply(this, arguments); +}; + // Listen SIGTERM and exit unless there is another listener process.on('SIGTERM', function () { if (process.listeners('SIGTERM').length === 1) process.exit(0); @@ -45,13 +55,16 @@ process.on('uncaughtException', function (err) { // If there's a custom uncaughtException handler expect it to terminate // the process. var hasCustomHandler = process.listeners('uncaughtException').length > 1; - var isTsError = err.message && /TypeScript/.test(err.message) - if (!hasCustomHandler && !isTsError) { + var isTsError = err.message && /TypeScript/.test(err.message); + if (!hasCustomHandler && !isTsError) { console.error(err.stack || err); - } + } + ipc.send({ error: isTsError ? '' : err.name || 'Error', message: err.message, + code: err.code, + lastRequire: lastRequire, willTerminate: hasCustomHandler }); }); diff --git a/package.json b/package.json index 4d0e482..5fd4da1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "test-docker": "docker run --rm -v ${PWD}:/app mhart/alpine-node:8.7.0 sh -c 'cd app && node ./bin/ts-node-dev --cache-directory .ts-node test/ts/big'" }, "dependencies": { + "chokidar": "^3.1.0", "dateformat": "~1.0.4-1.2.3", "dynamic-dedupe": "^0.3.0", "filewatcher": "~3.0.0", @@ -50,7 +51,7 @@ "mkdirp": "^0.5.1", "node-notifier": "^5.4.0", "resolve": "^1.0.0", - "rimraf": "^2.6.1", + "rimraf": "^2.7.1", "source-map-support": "^0.5.12", "tree-kill": "^1.2.1", "ts-node": "*",