├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── cmd.js ├── index.js ├── package.json └── test ├── demo.js ├── test.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test/bundle.js 7 | tmp.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md 11 | test/bundle.js 12 | test/test.html -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wtch 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | A small command-line app that watches for file changes and triggers a live-reload on file save (using [LiveReload](http://livereload.com/)). Watches the current working directory for `js,html,css` file changes. Ignores `.git`, `node_modules`, and `bower_components`, and other hidden files. 6 | 7 | ```sh 8 | npm install wtch -g 9 | 10 | #start watching .. 11 | wtch 12 | ``` 13 | 14 | You can use [garnish](https://github.com/mattdesl/garnish) for pretty-printing logs and limiting log level. 15 | 16 | ```js 17 | wtch | garnish --level debug 18 | ``` 19 | 20 | See [setup](#livereload-setup) for a basic how-to, and [tooling](#Tooling) for more advanced uses with browserify, watchify, etc. 21 | 22 | PRs/suggestions welcome. 23 | 24 | ## Usage 25 | 26 | [![NPM](https://nodei.co/npm/wtch.png)](https://www.npmjs.com/package/wtch) 27 | 28 | ```sh 29 | Usage: 30 | wtch [globs] [opts] 31 | 32 | Options: 33 | --dir -d current working directory to watch (defaults to cwd) 34 | --extension -e specifies an extension or a comma-separated list (default js,css,html) 35 | --event the type of event to watch, "all" or "change" (default "change") 36 | --port -p the port to run livereload (defaults to 35729) 37 | --poll enable polling for file watching 38 | ``` 39 | 40 | By default, it looks for `**/*` with the specified extensions. If `globs` is specified, they will *override* this behaviour. So you can do this to only watch a single file: 41 | 42 | ``` 43 | wtch bundle.js 44 | ``` 45 | 46 | ## API 47 | 48 | #### `live = wtch(glob, [opt])` 49 | 50 | Returns a through stream that watches the glob (or array of globs) and returns an event emitter. 51 | 52 | Supported options: 53 | 54 | - `cwd` the current working directory for chokidar 55 | - `poll` whether to use polling, default false 56 | - `event` the type of event to watch, can be `"change"` (default, only file save) or `"all"` (remove/delete/etc) 57 | - `port` the port for livereload, defaults to 35729 58 | - `ignoreReload` allows ignoring LiveReload events for specific files; can be a file path, or an array of paths, or a function that returns `true` to ignore the reload, Example: 59 | 60 | ```js 61 | wtch('**/*.js', { 62 | ignoreReload: function(file) { 63 | //don't trigger LiveReload for this file 64 | if (file === fileToIgnore) 65 | return true 66 | return false 67 | } 68 | }) 69 | //instead, manually decide what to do when that file changes 70 | .on('watch', handler) 71 | ``` 72 | 73 | #### `live.on('connect')` 74 | 75 | An event dispatched when the connection to live-reload server occurs. 76 | 77 | #### `live.on('watch')` 78 | 79 | An event dispatched when file change occurs. The first parameter is `event` type (e.g. `"change"`), the second is `file` path. 80 | 81 | #### `live.on('reload')` 82 | 83 | An event dispatched after the live reload trigger. First parameter will be the file path. 84 | 85 | ## LiveReload Setup 86 | 87 | There are two common ways of enabling LiveReload. 88 | 89 | #### Script Tag 90 | 91 | You can insert the following script tag in your HTML file. This will work across browsers and devices. 92 | 93 | ```html 94 | 95 | ``` 96 | 97 | Or you could use [inject-lr-script](https://github.com/mattdesl/inject-lr-script) to inject it while serving HTML content. 98 | 99 | #### Browser Plugin 100 | 101 | First, install the LiveReload plugin for your browser of choice (e.g. [Chrome](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en)). 102 | 103 | Now, install some tools globally. 104 | 105 | ```sh 106 | npm install wtch http-server garnish -g 107 | ``` 108 | 109 | Create a basic `index.html` file that references scripts and/or CSS files. 110 | 111 | Then, you can run your development server like so: 112 | 113 | ```sh 114 | http-server | wtch | garnish 115 | ``` 116 | 117 | Open `localhost:8080` and enable LiveReload by clicking the plugin. The center circle will turn black. You may need to refresh the page first. 118 | 119 | ![Click to enable](http://i.imgur.com/YdCgusY.png) 120 | 121 | Now when you save a JS/HTML/CSS file in the current directory, it will trigger a live-reload event on your `localhost:8080` tab. CSS files will be injected without a page refresh. 122 | 123 | ## Tooling 124 | 125 | This can be used for live-reloading alongside [wzrd](https://github.com/maxogden/wzrd), [beefy](https://github.com/maxogden/beefy) and similar development servers. For example: 126 | 127 | ```sh 128 | wzrd test/index.js | wtch --dir test -e js,css,es6 | garnish 129 | ``` 130 | 131 | It can also be used to augment [watchify](https://github.com/maxogden/watchify) with a browser live-reload event. This is better suited for larger bundles. 132 | 133 | ```sh 134 | watchify index.js -o bundle.js | wtch bundle.js 135 | ``` 136 | 137 | See [this package.json's](https://github.com/mattdesl/wtch/blob/master/package.json) script field for more detailed examples. 138 | 139 | ## License 140 | 141 | MIT, see [LICENSE.md](http://github.com/mattdesl/wtch/blob/master/LICENSE.md) for details. 142 | -------------------------------------------------------------------------------- /cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var argv = require('minimist')(process.argv.slice(2)) 3 | var wtch = require('./') 4 | var bole = require('bole') 5 | 6 | if (argv.p) 7 | argv.port = argv.p 8 | 9 | var extension = argv.e || argv.extension || ['js', 'html', 'css'] 10 | 11 | //if user passed globs, use those 12 | var globs = argv._.filter(Boolean) 13 | if (globs.length === 0) //otherwise wildcard w/ extensions 14 | globs = [ toExtension(extension) ] 15 | 16 | argv.cwd = argv.dir || argv.d 17 | 18 | //setup ndjson output 19 | bole.output({ 20 | level: 'debug', 21 | stream: process.stdout 22 | }) 23 | 24 | wtch(globs, argv) 25 | process.stdin.pipe(process.stdout) 26 | 27 | function toExtension(extension) { 28 | if (!Array.isArray(extension)) 29 | extension = [extension||''] 30 | 31 | //strip dots from extensions, split commas 32 | extension = extension 33 | .filter(Boolean) 34 | .map(function(e) { 35 | return e.split(',') 36 | }) 37 | .reduce(function(a, b) { 38 | return a.concat(b) 39 | }, []) 40 | .map(function(e) { 41 | if (e.indexOf('.')===0) 42 | return e.substring(1) 43 | return e 44 | }) 45 | 46 | //turn into a glob 47 | return extension.length === 0 48 | ? '**/*' 49 | : '**/*.{' + extension.join(',') + '}' 50 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend') 2 | var tinylr = require('tiny-lr') 3 | var watch = require('chokidar').watch 4 | var log = require('bole')('wtch') 5 | var Emitter = require('events/') 6 | var match = require('minimatch') 7 | 8 | var ignore = [ 9 | 'node_modules/**', 'bower_components/**', 10 | '.git', '.hg', '.svn', '.DS_Store', 11 | '*.swp', 'thumbs.db', 'desktop.ini' 12 | ] 13 | 14 | module.exports = function wtch(glob, opt) { 15 | opt = xtend({ 16 | port: 35729, 17 | host: 'localhost', 18 | event: 'change', 19 | ignored: ignore, 20 | ignoreInitial: true 21 | }, opt) 22 | 23 | if (typeof opt.port !== 'number') 24 | opt.port = 35729 25 | 26 | var ignoreReload = opt.ignoreReload 27 | var emitter = new Emitter() 28 | var server = tinylr() 29 | var closed = false 30 | var watcher 31 | 32 | if (opt.poll) 33 | opt.usePolling = true 34 | 35 | server.listen(opt.port, opt.host, function () { 36 | if (closed) 37 | return 38 | 39 | log.info('livereload running on '+opt.port) 40 | watcher = watch(glob, opt) 41 | watcher.on(opt.event, opt.event === 'all' 42 | ? reload 43 | : reload.bind(null, 'change')) 44 | 45 | emitter.emit('connect', server) 46 | }) 47 | 48 | function reload(event, path) { 49 | emitter.emit('watch', event, path) 50 | log.debug({ type: event, url: path }) 51 | 52 | if ((Array.isArray(ignoreReload) || typeof ignoreReload === 'string') 53 | && reject(path, ignoreReload)) { 54 | return 55 | } else if (typeof ignoreReload === 'function' && ignoreReload(path)) { 56 | return 57 | } 58 | 59 | try { 60 | server.changed({ body: { files: [ path ] } }) 61 | emitter.emit('reload', path) 62 | } catch (e) { 63 | throw e 64 | } 65 | } 66 | 67 | var serverImpl = server.server 68 | serverImpl.removeAllListeners('error') 69 | serverImpl.on('error', function(err) { 70 | if (err.code === 'EADDRINUSE') { 71 | process.stderr.write('ERROR: livereload not started, port '+opt.port+' is in use\n') 72 | server.close() 73 | } 74 | }) 75 | 76 | emitter.close = function() { 77 | server.close() 78 | if (watcher) 79 | watcher.close() 80 | closed = true 81 | } 82 | 83 | return emitter 84 | } 85 | 86 | function reject(file, ignores) { 87 | if (!ignores) 88 | return false 89 | if (!Array.isArray(ignores)) 90 | ignores = [ ignores ] 91 | return ignores.some(function(ignore) { 92 | return match(file, ignore) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtch", 3 | "version": "4.0.1", 4 | "description": "small livereload/watch command line utility", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "bin": { 8 | "wtch": "./cmd.js" 9 | }, 10 | "author": { 11 | "name": "Matt DesLauriers", 12 | "email": "dave.des@gmail.com", 13 | "url": "https://github.com/mattdesl" 14 | }, 15 | "dependencies": { 16 | "bole": "^2.0.0", 17 | "chokidar": "^1.2.0", 18 | "events": "^1.0.2", 19 | "minimatch": "^2.0.1", 20 | "minimist": "^1.1.0", 21 | "through2": "^0.6.3", 22 | "tiny-lr": "^0.1.5", 23 | "xtend": "^4.0.0" 24 | }, 25 | "devDependencies": { 26 | "browserify": "^8.1.3", 27 | "garnish": "^2.0.0", 28 | "tape": "^3.5.0", 29 | "watchify": "^2.3.0", 30 | "wzrd": "^1.2.1" 31 | }, 32 | "scripts": { 33 | "demo": "wzrd test/demo.js | ./cmd.js | garnish --level debug", 34 | "watcher": "watchify test/demo.js -o test/bundle.js --verbose", 35 | "live": "./cmd.js test/bundle.js | garnish", 36 | "incremental": "npm run watcher | npm run live", 37 | "test": "node test/test.js" 38 | }, 39 | "keywords": [ 40 | "watch", 41 | "watcher", 42 | "dev", 43 | "server", 44 | "livereload", 45 | "live", 46 | "reload" 47 | ], 48 | "repository": { 49 | "type": "git", 50 | "url": "git://github.com/mattdesl/wtch.git" 51 | }, 52 | "homepage": "https://github.com/mattdesl/wtch", 53 | "bugs": { 54 | "url": "https://github.com/mattdesl/wtch/issues" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/demo.js: -------------------------------------------------------------------------------- 1 | console.log("Try s modifying this script!") -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | example 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var wtch = require('../') 3 | var fs = require('fs') 4 | var path = require('path') 5 | 6 | test('should connect to live reload server', function(t) { 7 | var filepath = path.join(__dirname, 'tmp.txt') 8 | var live = wtch(filepath) 9 | t.plan(4) 10 | live.on('connect', function() { 11 | t.ok(true, 'connected to server') 12 | setTimeout(function() { 13 | fs.writeFileSync(filepath, ''+new Date()) 14 | }, 10) 15 | }) 16 | 17 | live.on('watch', function(event, file) { 18 | t.equal(event, 'change', 'received change event') 19 | t.equal(file, filepath, 'received change file') 20 | }) 21 | 22 | live.on('reload', function(file) { 23 | t.equal(file, filepath, 'received livereload trigger') 24 | live.close() 25 | }) 26 | }) 27 | 28 | 29 | test('should allow ignoring live reload events', function(t) { 30 | var filepath = path.join(__dirname, 'tmp.txt') 31 | var live = wtch(filepath, { ignoreReload: filepath }) 32 | t.plan(3) 33 | live.on('connect', function() { 34 | t.ok(true, 'connected to server') 35 | setTimeout(function() { 36 | fs.writeFileSync(filepath, ''+new Date()) 37 | }, 300) 38 | }) 39 | 40 | live.on('watch', function(event, file) { 41 | t.equal(event, 'change', 'received change event') 42 | t.equal(file, filepath, 'received change file') 43 | live.close() 44 | }) 45 | 46 | live.on('reload', function(file) { 47 | t.fail('should not have received the live reload event') 48 | }) 49 | }) 50 | 51 | test('should allow ignoring live reload events', function(t) { 52 | t.plan(4) 53 | var filepath = path.join(__dirname, 'tmp.txt') 54 | var live = wtch(filepath, { 55 | ignoreReload: function(file) { 56 | t.equal(file, filepath, 'about to ignore file') 57 | return file === filepath 58 | } 59 | }) 60 | live.on('connect', function() { 61 | t.ok(true, 'connected to server') 62 | setTimeout(function() { 63 | fs.writeFileSync(filepath, ''+new Date()) 64 | }, 10) 65 | }) 66 | 67 | live.on('watch', function(event, file) { 68 | t.equal(event, 'change', 'received change event') 69 | t.equal(file, filepath, 'received change file') 70 | live.close() 71 | }) 72 | 73 | live.on('reload', function(file) { 74 | t.fail('should not have received the live reload event') 75 | }) 76 | }) 77 | --------------------------------------------------------------------------------