├── .gitignore ├── LICENSE.md ├── README.md ├── lib ├── launcher.js └── piping.js ├── package.json └── src ├── launcher.coffee └── piping.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | test 14 | 15 | npm-debug.log 16 | node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Michael Lawson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Piping 2 | 3 | There are already node "wrappers" that handle watching for file changes and restarting your application (such as [node-supervisor](https://github.com/isaacs/node-supervisor)), as well as reloading on crash, but I wasn't fond of having that. 4 | Piping adds "hot reloading" functionality to node, watching all your project files and reloading when anything changes, without requiring a "wrapper" binary. 5 | 6 | Piping uses the currently unstable "cluster" API to spawn your application in a thread and then kill/reload it when necessary. Because of this, piping should be considered unstable and should not be used in production (why would you ever need live code reloading in production anyway). Currently, at least on windows, the cluster API seems stable enough for development. 7 | 8 | Also check out [piping-browser](http://github.com/mdlawson/piping-browser) which does a similar job for the browser using browserify 9 | 10 | ## Installation 11 | ``` 12 | npm install piping 13 | ``` 14 | ## Usage 15 | 16 | Piping is not a binary, so you can continue using your current workflow for running your application ("wooo!"). Basic usage is as follows: 17 | ```javascript 18 | if (require("piping")()) { 19 | // application logic here 20 | express = require("express"); 21 | app = express(); 22 | app.listen(3000); 23 | } 24 | ``` 25 | or in coffeescript: 26 | ```coffee 27 | if require("piping")() 28 | # application logic here 29 | express = require "express" 30 | app = express() 31 | app.listen 3000 32 | ``` 33 | This if condition is necessary because your file will be invoked twice, but should only actually do anything the second time, when it is spawned as a separate node process, supervised by piping. Piping returns true when its good to go. 34 | 35 | the function returned by piping also accepts an options object. The following options are supported: 36 | - __main__ _(path)_: The path to the "top" file of your application. Defaults to `require.main.filename`, which should be sufficient provided you launch your application via "node yourapp.js". Other launch methods may require this to be set manually. If your app doesn't reload/reloads when it shouldn't, try changing this. 37 | - __hook__ _(true/false)_: Whether to hook into node's "require" function and only watch required files. Defaults to false, which means piping will watch all the files in the folder in which main resides. The require hook can only detect files required after invoking this module! 38 | - __includeModules__ _(true/false)_: Whether to include required files than reside in node_modules folders. Defaults to false. Only has an effect when hook is true. For ignoring node_modules when hook is false, please use ignore. 39 | - __ignore__ _(regex)_: Files/paths matching this regex will not be watched. Defaults to `/(\/\.|~$)/` 40 | - __language__ _(string)_: The name of a module that will be required before your main is invoked. This allows for "coffee-script" to be specified to support a coffeescript main, launchable though "coffee". Probably works for other languages as well. Coffeescripters don't actually need this, as coffee-script is required automatically if main is a .coffee file. 41 | - __usePolling__ _(true/false)_ : From chokidar. Default false. Whether to use fs.watchFile (backed by polling), or fs.watch. It is typically necessary to set this to true to successfully watch files over a network. 42 | - __interval__ _(true/false)_ : From chokidar. Polling specific. Interval of file system polling (default 100). 43 | - __binaryInterval__ _(true/false)_ : From chokidar. Polling specific. Interval of file system polling for binary files. 44 | - __respawnOnExit__ _(true/false)_ : Default true. Whether the application should respawn after exiting. If you experience problems with infinite loops, try setting this to false. 45 | 46 | Example: 47 | ```javascript 48 | if (require("piping")({main:"./app/server.js",hook:true})){ 49 | // app logic 50 | } 51 | ``` 52 | Piping can also be used just by passing a string. In this case, the string is taken to be the "main" option: 53 | ```javascript 54 | if (require("piping")("./app/server.js")){ 55 | // app logic 56 | } 57 | ``` 58 | One negative of all the examples above is the extra indent added to your code. To avoid this, you can choose to return when piping is false: 59 | 60 | ```javascript 61 | if (!require("piping")()) { return; } 62 | // application logic here 63 | ``` 64 | or in coffeescript: 65 | ```coffee 66 | if not require("piping")() then return 67 | # application logic here 68 | ``` 69 | -------------------------------------------------------------------------------- /lib/launcher.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | (function() { 3 | var cluster, languages, natives, path, 4 | indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 5 | 6 | cluster = require("cluster"); 7 | 8 | path = require("path"); 9 | 10 | natives = ['assert', 'buffer', 'child_process', 'cluster', 'console', 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'freelist', 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'tty', 'url', 'util', 'vm', 'zlib']; 11 | 12 | languages = { 13 | ".coffee": "coffee-script" 14 | }; 15 | 16 | cluster.worker.on("message", function(options) { 17 | var _load_orig, ext, main, module; 18 | main = path.resolve(process.cwd(), options.main); 19 | if (options.hook) { 20 | module = require("module"); 21 | _load_orig = module._load; 22 | module._load = function(name, parent, isMain) { 23 | var file; 24 | file = module._resolveFilename(name, parent); 25 | if (options.includeModules || file.indexOf("node_modules") === -1) { 26 | if (!(indexOf.call(natives, file) >= 0 || file === main)) { 27 | cluster.worker.send({ 28 | file: file 29 | }); 30 | } 31 | } 32 | return _load_orig(name, parent, isMain); 33 | }; 34 | } 35 | ext = path.extname(options.main); 36 | if (languages[ext]) { 37 | require(languages[ext]); 38 | } 39 | if (options.language) { 40 | require(options.language); 41 | } 42 | return require(main); 43 | }); 44 | 45 | process.on("uncaughtException", function(err) { 46 | cluster.worker.send({ 47 | err: (err != null ? err.stack : void 0) || err 48 | }); 49 | return cluster.worker.kill(); 50 | }); 51 | 52 | }).call(this); 53 | -------------------------------------------------------------------------------- /lib/piping.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.10.0 2 | (function() { 3 | var cluster, colors, options, path; 4 | 5 | cluster = require("cluster"); 6 | 7 | path = require("path"); 8 | 9 | colors = require("colors"); 10 | 11 | options = { 12 | hook: false, 13 | includeModules: false, 14 | main: require.main.filename, 15 | ignore: /(\/\.|~$)/, 16 | respawnOnExit: true 17 | }; 18 | 19 | module.exports = function(ops) { 20 | var chokidar, fixChokidar, initial, key, lastErr, respawnPending, value, watcher; 21 | if (typeof ops === "string" || ops instanceof String) { 22 | options.main = path.resolve(ops); 23 | } else { 24 | for (key in ops) { 25 | value = ops[key]; 26 | options[key] = value; 27 | } 28 | } 29 | if (cluster.isMaster) { 30 | cluster.setupMaster({ 31 | exec: path.join(path.dirname(module.filename), "launcher.js") 32 | }); 33 | chokidar = require("chokidar"); 34 | fixChokidar = function(file) { 35 | return file.slice(0, -1) + "[" + file.slice(-1) + "]"; 36 | }; 37 | initial = options.hook ? fixChokidar(options.main) : path.dirname(options.main); 38 | watcher = chokidar.watch(initial, { 39 | ignored: options.ignore, 40 | ignoreInitial: true, 41 | usePolling: options.usePolling, 42 | interval: options.interval || 100, 43 | binaryInterval: options.binaryInterval || 300 44 | }); 45 | cluster.fork(); 46 | respawnPending = false; 47 | lastErr = ""; 48 | cluster.on("exit", function(dead, code, signal) { 49 | var hasWorkers, id, ref, worker; 50 | hasWorkers = false; 51 | ref = cluster.workers; 52 | for (id in ref) { 53 | worker = ref[id]; 54 | hasWorkers = true; 55 | } 56 | if (!hasWorkers && (respawnPending || options.respawnOnExit)) { 57 | cluster.fork(); 58 | return respawnPending = false; 59 | } 60 | }); 61 | cluster.on("online", function(worker) { 62 | worker.send(options); 63 | return worker.on("message", function(message) { 64 | if (message.err && (!options.respawnOnExit || message.err !== lastErr)) { 65 | console.log("[piping]".bold.red, "can't execute file:", options.main); 66 | console.log("[piping]".bold.red, "error given was:", message.err); 67 | if (options.respawnOnExit) { 68 | lastErr = message.err; 69 | return console.log("[piping]".bold.red, "further repeats of this error will be suppressed..."); 70 | } 71 | } else if (message.file) { 72 | if (options.usePolling) { 73 | return watcher.add(message.file); 74 | } else { 75 | return watcher.add(fixChokidar(message.file)); 76 | } 77 | } 78 | }); 79 | }); 80 | watcher.on("change", function(file) { 81 | var id, ref, worker; 82 | console.log("[piping]".bold.red, "File", path.relative(process.cwd(), file), "has changed, reloading."); 83 | ref = cluster.workers; 84 | for (id in ref) { 85 | worker = ref[id]; 86 | respawnPending = true; 87 | process.kill(worker.process.pid, 'SIGTERM'); 88 | } 89 | if (!respawnPending) { 90 | return cluster.fork(); 91 | } 92 | }); 93 | return false; 94 | } else { 95 | return true; 96 | } 97 | }; 98 | 99 | }).call(this); 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piping", 3 | "version": "0.3.1", 4 | "description": "Keep your code piping hot! Live code reloading without additional binaries", 5 | "main": "lib/piping.js", 6 | "dependencies": { 7 | "chokidar": "^1.1.0", 8 | "colors": "1.0.x" 9 | }, 10 | "devDependencies": { 11 | "coffee-script": "~1.6.0" 12 | }, 13 | "repository": "git://github.com/mdlawson/piping.git", 14 | "keywords": [ 15 | "live", 16 | "code", 17 | "reload", 18 | "hot" 19 | ], 20 | "author": "Michael Lawson", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /src/launcher.coffee: -------------------------------------------------------------------------------- 1 | cluster = require "cluster" 2 | path = require "path" 3 | natives = ['assert','buffer','child_process','cluster','console','constants','crypto','dgram','dns','domain','events','freelist','fs','http','https','module','net','os','path','punycode','querystring','readline','repl','stream','string_decoder','sys','timers','tls','tty','url','util','vm','zlib'] 4 | languages = 5 | ".coffee":"coffee-script" 6 | 7 | cluster.worker.on "message", (options) -> 8 | main = path.resolve process.cwd(), options.main 9 | if options.hook 10 | module = require "module" 11 | _load_orig = module._load 12 | 13 | module._load = (name,parent,isMain) -> 14 | file = module._resolveFilename name,parent 15 | 16 | if options.includeModules or file.indexOf("node_modules") is -1 17 | unless file in natives or file is main 18 | cluster.worker.send file:file 19 | 20 | _load_orig name,parent,isMain 21 | ext = path.extname options.main 22 | if languages[ext] then require languages[ext] 23 | if options.language then require options.language 24 | 25 | require main 26 | 27 | process.on "uncaughtException", (err) -> 28 | cluster.worker.send 29 | err: err?.stack || err 30 | cluster.worker.kill() 31 | -------------------------------------------------------------------------------- /src/piping.coffee: -------------------------------------------------------------------------------- 1 | cluster = require "cluster" 2 | path = require "path" 3 | colors = require "colors" 4 | 5 | options = 6 | hook: false 7 | includeModules: false 8 | main: require.main.filename 9 | ignore: /(\/\.|~$)/ 10 | respawnOnExit: true 11 | 12 | module.exports = (ops) -> 13 | if typeof ops is "string" or ops instanceof String 14 | options.main = path.resolve ops 15 | else 16 | options[key] = value for key,value of ops 17 | 18 | if cluster.isMaster 19 | cluster.setupMaster 20 | exec: path.join(path.dirname(module.filename),"launcher.js") 21 | 22 | chokidar = require "chokidar" 23 | 24 | # Workaround for https://github.com/paulmillr/chokidar/issues/237 25 | fixChokidar = (file) -> 26 | file.slice(0, -1) + "["+file.slice(-1)+"]" 27 | 28 | initial = if options.hook then fixChokidar options.main else path.dirname options.main 29 | 30 | watcher = chokidar.watch initial, 31 | ignored: options.ignore 32 | ignoreInitial: true 33 | usePolling: options.usePolling 34 | interval: options.interval || 100 35 | binaryInterval: options.binaryInterval || 300 36 | 37 | cluster.fork() 38 | respawnPending = false 39 | lastErr = "" 40 | 41 | 42 | cluster.on "exit", (dead,code,signal) -> 43 | hasWorkers = false 44 | for id, worker of cluster.workers 45 | hasWorkers = true 46 | 47 | if !hasWorkers && (respawnPending || options.respawnOnExit) 48 | cluster.fork() 49 | respawnPending = false 50 | 51 | 52 | cluster.on "online", (worker) -> 53 | worker.send options 54 | worker.on "message", (message) -> 55 | if message.err && (!options.respawnOnExit || message.err isnt lastErr) 56 | console.log "[piping]".bold.red,"can't execute file:",options.main 57 | console.log "[piping]".bold.red,"error given was:",message.err 58 | if options.respawnOnExit 59 | lastErr = message.err 60 | console.log "[piping]".bold.red,"further repeats of this error will be suppressed..." 61 | else if message.file 62 | if options.usePolling 63 | watcher.add message.file 64 | else 65 | watcher.add (fixChokidar message.file) 66 | 67 | 68 | watcher.on "change", (file) -> 69 | console.log "[piping]".bold.red,"File",path.relative(process.cwd(),file),"has changed, reloading." 70 | 71 | # if a worker is already running, kill it and let the exit handler respawn it 72 | for id, worker of cluster.workers 73 | respawnPending = true 74 | process.kill(worker.process.pid, 'SIGTERM') # worker.kill() doesn't send SIGTERM 75 | 76 | # if a worker died somehow, respawn it right away 77 | unless respawnPending 78 | cluster.fork() 79 | 80 | return false 81 | else 82 | return true 83 | --------------------------------------------------------------------------------