├── .npmignore ├── index.js ├── manager.js ├── node-dev.sh ├── package.json └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .git* 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./manager.js') 2 | -------------------------------------------------------------------------------- /manager.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | , fs = require('fs') 3 | , spawn = require('child_process').spawn 4 | , Inotify = require('inotify').Inotify 5 | , inotify = new Inotify() 6 | 7 | 8 | module.exports = function(options) { 9 | return new NodeManager(options) 10 | } 11 | 12 | /** 13 | * The manager which watches files and restarts 14 | * Main options: 15 | * run {string!}: the js-file to run 16 | * e.g "./app.js", no default, required 17 | * 18 | * watchDir {string} the folder to watch, default: '.' 19 | * 20 | * ignoredPaths {array}: array of ignored paths, which shouldn't not be watched 21 | * members can be 22 | * strings (matched as ===), 23 | * regexps (like extension check) or 24 | * function(p) { returns true if path p should be ignored } 25 | * default: [] 26 | * 27 | * 28 | * Logging options 29 | * debug {bool}: adds additional output about watches and changes, default: false 30 | * logger {object}: custom logger, must have error and debug methods 31 | * 32 | */ 33 | function NodeManager(options) { 34 | 35 | // public interface 36 | this.watchFile = watchFile 37 | this.watchFolder = watchFolder 38 | this.run = run 39 | this.start = start 40 | 41 | if (!options.run) { 42 | throw new Error("Please provide what to run. E.g. new NodeManager({ run: './app.js' })") 43 | } 44 | 45 | var ignoredPaths = options.ignoredPaths || [] 46 | 47 | var logger = options.logger || { 48 | debug: function() { 49 | options.debug && console.log.apply(console, arguments) 50 | }, 51 | 52 | error: function() { 53 | console.log.apply(console, arguments) 54 | } 55 | } 56 | 57 | var onRunOutput = options.onRunOutput || function(data) { 58 | util.print(data); 59 | } 60 | 61 | var onRunError = options.onRunError || function(data) { 62 | util.print(data); 63 | } 64 | 65 | /** 66 | * Run with default watch config 67 | */ 68 | function start() { 69 | var dir = options.watchDir || '.' 70 | 71 | this.run(); 72 | this.watchFile(dir); // watch current folder 73 | this.watchFolder(dir); // watch all files under current folder 74 | 75 | process.stdin.resume(); 76 | process.stdin.setEncoding('utf8'); 77 | } 78 | 79 | var child // references the child process, after spawned 80 | 81 | // Kill child process before ending 82 | process.on('SIGTERM', function () { 83 | if (child) { 84 | child.kill(); 85 | } 86 | 87 | process.exit(); 88 | }); 89 | 90 | /** 91 | * executes the command given by the argument 92 | */ 93 | function run() { 94 | // run the server 95 | var node = options.node || options.run.match(/\.coffee$/) ? 'coffee' : 'node' 96 | 97 | child = spawn(node, [options.run].concat(process.argv.slice(3))); 98 | 99 | // let the child's output escape. 100 | child.stdout.on('data', onRunOutput); 101 | child.stderr.on('data', onRunError); 102 | 103 | // let the user's typing get to the child 104 | process.stdin.pipe(child.stdin); 105 | 106 | console.log('\nStarting: ' + options.run); 107 | } 108 | 109 | var restartIsScheduled 110 | , restartTimer 111 | , lastRestartTime 112 | 113 | 114 | /** 115 | * restarts the server 116 | * doesn't restart more often than once per 10 ms 117 | * suspends restart a little bit in case of massive changes to save CPU 118 | */ 119 | function restart() { 120 | 121 | if (restartIsScheduled) return 122 | 123 | if (lastRestartTime && lastRestartTime + 500 > new Date) { 124 | // if last restart was recent, we postpone new restart a bit to save resources, 125 | // because it often means that many files are changed at once 126 | schedule(150) 127 | } else { 128 | // in any way we schedule restart in next 10 ms, so many change events on one file/dir lead to single restart 129 | schedule(10) 130 | } 131 | 132 | function schedule(ms) { 133 | restartIsScheduled = true 134 | setTimeout(function() { 135 | restartIsScheduled = false 136 | doRestart() 137 | }, ms) 138 | } 139 | 140 | function doRestart() { 141 | lastRestartTime = +new Date 142 | // kill if running 143 | if (child) child.kill(); 144 | // run it again 145 | run(); 146 | } 147 | } 148 | 149 | 150 | /** 151 | * watches all files and subdirectories in given folder (recursively), 152 | * excluding the folder itself 153 | * @param root 154 | */ 155 | function watchFolder(root) { 156 | var files = fs.readdirSync(root); 157 | 158 | files.forEach(function(file) { 159 | var path = root + '/' + file 160 | , stat = fs.statSync(path); 161 | 162 | // watch file/folder 163 | if (isIgnoredPath(path)) { 164 | logger.debug("ignored path " + path) 165 | return 166 | } 167 | 168 | // ignore absent files 169 | if (!stat) { 170 | logger.error("ERROR: couldn't stat " + path) 171 | return 172 | } 173 | 174 | watchFile(path); 175 | 176 | // recur if directory 177 | if (stat.isDirectory()) { 178 | watchFolder(path); 179 | } 180 | }); 181 | 182 | } 183 | 184 | function isIgnoredPath(path) { 185 | 186 | for (var i = 0; i < ignoredPaths.length; i++) { 187 | var test = ignoredPaths[i]; 188 | 189 | logger.debug("isIgnoredPath: path " + path + " tested against " + test); 190 | if (test === path) return true; 191 | if (test instanceof RegExp && test.test(path)) return true; 192 | if (test instanceof Function && test(path)) return true; 193 | } 194 | 195 | logger.debug("path is ok: " + path) 196 | return false; 197 | } 198 | 199 | // arrays for debug events output 200 | var flag2event = {} 201 | var eventsAll = [ 202 | "IN_ACCESS", "IN_ATTRIB", "IN_CLOSE", "IN_CLOSE_NOWRITE", "IN_CLOSE_WRITE", "IN_CREATE", 203 | "IN_DELETE", "IN_DELETE_SELF", "IN_DONT_FOLLOW", "IN_IGNORED", "IN_ISDIR", 204 | "IN_MASK_ADD", "IN_MODIFY", "IN_MOVE", "IN_MOVED_FROM", "IN_MOVED_TO", "IN_MOVE_SELF", 205 | "IN_ONESHOT", "IN_ONLYDIR", "IN_OPEN", "IN_Q_OVERFLOW", "IN_UNMOUNT" 206 | ] 207 | eventsAll.forEach(function(event) { 208 | flag2event[Inotify[event]] = event 209 | }) 210 | 211 | 212 | function watchFile(file) { 213 | 214 | logger.debug("Watch: " + file); 215 | 216 | function callback(event) { 217 | try { 218 | event.toString = function() { 219 | // translate event flags into string 220 | // e.g IN_CLOSE | IN_CLOSE_WRITE 221 | var txt = [] 222 | eventsAll.forEach(function(e) { 223 | if (Inotify[e] & this.mask) txt.push(e) 224 | }.bind(this)) 225 | return txt.join(' | ') 226 | } 227 | 228 | if (event.name) { // name of updated file inside watched folder 229 | var path = file + '/' + event.name 230 | if (isIgnoredPath(path)) { 231 | return 232 | } 233 | 234 | if (event.mask & Inotify.IN_CREATE) { // IN_CREATE always comes here 235 | // when a new file is created we wait for it's close_write 236 | watchFile(path) 237 | if (fs.statSync(path).isDirectory()) { 238 | watchFolder(path) 239 | } 240 | return 241 | } 242 | 243 | logger.debug(path + ' ' + event); 244 | 245 | restart() 246 | 247 | } else { 248 | logger.debug(file + ' ' + event); 249 | 250 | restart() 251 | } 252 | } 253 | catch (exception) { 254 | // Ignore exceptions caused by missing files (temp files) 255 | if (exception.code !== 'ENOENT') { 256 | throw exception; 257 | } 258 | } 259 | } 260 | 261 | 262 | // we watch for these events for files and folders 263 | // there may be many IN_MODIFY, so we don't watch it, but we await for IN_CLOSE_WRITE 264 | var watchEvents = [ 'IN_CLOSE_WRITE' , 'IN_CREATE' , 'IN_DELETE' , 'IN_DELETE_SELF' , 'IN_MOVE_SELF', 'IN_MOVE' ]; 265 | 266 | inotify.addWatch({ 267 | path: file, 268 | watch_for: watchEvents.reduce(function(prev, cur) { 269 | return prev | Inotify[cur] 270 | }, 0), 271 | callback: callback 272 | }) 273 | 274 | } 275 | 276 | 277 | } 278 | -------------------------------------------------------------------------------- /node-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | if (process.argv.length < 3) { 4 | console.log('Found 1 argument. Expected at least two.'); 5 | console.log('Usage: \n node-dev app.js'); 6 | process.exit(1); 7 | } 8 | 9 | var manager = require('./index')({ 10 | ignoredPaths: [ 11 | './public', // static folder for an express project 12 | /\.dirtydb$/, /\.db$/, // sqlite db 13 | /\/\./ // all files and directories starting with dot 14 | ], 15 | run: process.argv[2] 16 | }) 17 | 18 | 19 | manager.start(); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "description": "Reruns the given file whenever the current working dir subtree has modifications.", 4 | "version": "0.1.3", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/iliakan/node-dev.git" 8 | }, 9 | "author": { 10 | "name": "Ilya Kantor", 11 | "email": "iliakan@gmail.com", 12 | "url": "http://javascript.info/" 13 | }, 14 | "preferGlobal": true, 15 | "bin": { 16 | "node-dev": "./node-dev.sh" 17 | }, 18 | "dependencies": { 19 | "inotify": ">= 0.1.6" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Deprecated 3 | 4 | As of now, this module is deprecated. It worked great for me, but it's Linux only, and now I'm on Mac. 5 | 6 | I recommend https://github.com/remy/nodemon. 7 | 8 | # Linux only 9 | 10 | The module relies on inotify, only available for Linux. 11 | 12 | For other OS I'd recommend supervisor or nodemon. 13 | 14 | # What does it do? 15 | 16 | It autoreloads Node.JS in case of any file changes. 17 | 18 | 19 | $ npm install dev -g 20 | 21 | $ node-dev app.js 22 | 23 | Starting: app.js 24 | > server is listening on http://127.0.0.1:8080 25 | 26 | `node-dev` will rerun `app.js` whenever one of the watched files is 27 | changed. 28 | 29 | The module is based on inotify. So, unlike most other modules of this kind, *it starts watching new files automatically*. 30 | 31 | A number of additional options make the module really flexible and extendible. 32 | 33 | ## Install 34 | 35 | `npm install dev -g` 36 | 37 | Global installation is preferred to have `node-dev` utility in path. 38 | 39 | ### Simple usage 40 | 41 | If you've installed it globally, then there is a `node-dev` on path, so chdir to your app and run it: 42 | 43 | $ node-dev app.js 44 | 45 | Starting: app.js 46 | > server is listening on http://127.0.0.1:8080 47 | 48 | Then go to your IDE and edit files. `node-dev` keeps your app up-to-date. The only need to switch to terminal is when there are errors. 49 | 50 | But even if there are errors, you can switch back to IDE, correct them and `node-dev` will autorestart the server again for you. 51 | 52 | By default, files under `./public`, files with extensions `.db, .dirtydb`, files and directories starting with dot `.` are not watched. 53 | 54 | ### Advanced usage 55 | 56 | The `node-dev` utility is a tiny file which basically contains two lines: 57 | 58 | var manager = require("dev")(options); 59 | manager.start(); 60 | 61 | You can copy and modify it, or create your own, more featured autorestarter on it's base. 62 | The `options` object may have following properties: 63 | 64 | #### Running 65 | 66 | - `run`: the js file to run, e.g `./app.js`, it is the only required option. 67 | 68 | #### Watch/Ignore 69 | 70 | - `watchDir`: the folder to watch recursively, default: `.` 71 | - `ignoredPaths` [ paths ]: array of ignored paths, which are not watched, members can be: 72 | * `string`, matched exactly against path, like `./public`, 73 | * `RegExp`, e.g an extension check: `/\.gif$/` 74 | * `function(path)`, which takes the path and returns `true` if it should be ignored 75 | 76 | #### Logging 77 | - `debug`: enables additional logging output about watches and changes, default: `false` 78 | - `logger`: custom logger object, must have `error(...)` and `debug(...)` methods, delegates to `console.log` by default. Can use any other logger. 79 | 80 | #### Info 81 | - `onRunOutput`: callback `function(output)`, called for `stdout` data from the running process 82 | - `onRunError`: callback `function(output)`, called for `stderr` data from the running process 83 | 84 | You can use these to send error notifications and integrate with your development environment if you wish. 85 | 86 | ### Troubleshooting 87 | 88 | This module doesn't compile/run on non-Linux OS. See the head of this file for the details. 89 | 90 | There are limits on the number of watched files in inotify. For example, Debian has 8192 by default. In most cases, that should be enough. If it's not, and you really really need to watch so many files, then you can adjust the limit. 91 | 92 | To change the limit: 93 | 94 | $ echo 16384 > /proc/sys/fs/inotify/max_user_watches 95 | 96 | Or: 97 | 98 | $ sudo sysctl fs.inotify.max_user_watches=16364 99 | 100 | To make the change permanent, edit the file `/etc/sysctl.conf` and add this line to the end of the file: 101 | 102 | fs.inotify.max_user_watches=16384 103 | 104 | 105 | 106 | --------------------------------------------------------------------------------