├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── example ├── app.js └── server.js ├── index.js ├── lib ├── bundler.js └── parse-error.js ├── package-lock.json ├── package.json └── test ├── fixture-err.js ├── fixture-watch.js ├── fixture.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | # watchify-middleware 2 | 3 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 4 | 5 | A simple middleware for watchify which provides a few features for a better development experience: 6 | 7 | - suspends the server response so you are never served a stale or empty bundle 8 | - removes the default 600ms delay (left up to the developer to reconfigure) 9 | - emits timing information in a `'log'` event 10 | - (optional) allows for a browser-based error handler (eg: print to DevTools console) 11 | 12 | For practical implementations, see [watchify-server](https://www.npmjs.com/package/watchify-server) or [budo](https://www.npmjs.com/package/budo). 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install watchify-middleware --save 18 | ``` 19 | 20 | ## Example 21 | 22 | ```js 23 | var watchifyMiddleware = require('watchify-middleware') 24 | var defaultIndex = require('simple-html-index') 25 | 26 | var staticUrl = 'bundle.js' 27 | var bundler = browserify('app.js', { 28 | // config for watchify 29 | cache: {}, 30 | packageCache: {}, 31 | basedir: __dirname 32 | }) 33 | var watchify = watchifyMiddleware(bundler) 34 | 35 | var server = http.createServer(function (req, res) { 36 | if (req.url === '/') { 37 | defaultIndex({ entry: staticUrl }).pipe(res) 38 | } else if (req.url === '/' + staticUrl) { 39 | watchify(req, res) 40 | } 41 | }) 42 | 43 | server.listen(8000, 'localhost', function () { 44 | console.log('Listening on http://localhost:8000/') 45 | }) 46 | ``` 47 | 48 | For a more complete example, see [example/server.js](example/server.js). 49 | 50 | ## Usage 51 | 52 | [![NPM](https://nodei.co/npm/watchify-middleware.png)](https://www.npmjs.com/package/watchify-middleware) 53 | 54 | #### `middleware = watchifyMiddleware(browserify[, opt])` 55 | 56 | Returns a `middleware(req, res)` function from the given `browserify` bundler instance and options: 57 | 58 | - `delay` (default 0) a delay to debounce the rebuild, useful for things like git branch switches (where hundreds of files may change at once) 59 | - `errorHandler` (default false) a boolean or function for handling errors 60 | - `initialBundle` (default true) whether to initially bundle and emit `'pending'` 61 | 62 | `errorHandler` can be a function that accepts `(err)` parameter and optionally returns the new contents (String|Buffer) of the JavaScript bundle. If `errorHandler` is `true`, it will default to the following: 63 | 64 | ```js 65 | var stripAnsi = require('strip-ansi') 66 | 67 | function defaultErrorHandler (err) { 68 | console.error('%s', err) 69 | var msg = stripAnsi(err.message) 70 | return ';console.error(' + JSON.stringify(msg) + ');' 71 | } 72 | ``` 73 | 74 | (some plugins produce ANSI color codes in error messages) 75 | 76 | Otherwise, it assumes the normal behaviour for error handling (which is typically just an uncaught error event). 77 | 78 | #### `emitter = watchifyMiddleware.emitter(browserify[, opt])` 79 | 80 | The same as above, except this returns an EventEmitter for handling bundle updates. 81 | 82 | ##### `emitter.middleware` 83 | 84 | The `middleware(req, res)` function for use in your server. 85 | 86 | ##### `emitter.bundle()` 87 | 88 | Triggers a bundle event. Usually should only be called if `initialBundle` is set to false, to trigger the initial bundle. 89 | 90 | ##### `emitter.on('pending', fn)` 91 | 92 | Called when watchify begins its incremental rebuild. 93 | 94 | ##### `emitter.on('update', fn)` 95 | 96 | Called when bundling is finished, with parameter `(contents, rows)`. 97 | 98 | `contents` is a Buffer/String of the bundle and `rows` is a list of dependencies that have changed since last update. On first run, this will be an empty array. 99 | 100 | ##### `emitter.on('log', fn)` 101 | 102 | Provides timing and server request logging, passing an `(event)` parameter. 103 | 104 | Server request logs look like this: 105 | 106 | ```js 107 | { level: 'debug', type: 'request', message: 'bundle (pending|ready)'} 108 | ``` 109 | 110 | Bundle updates look like this: 111 | 112 | ```js 113 | { elapsed: Number, level: 'info', type: 'bundle' } 114 | ``` 115 | 116 | These events work well with [garnish](https://github.com/mattdesl/garnish) and other ndjson-based tools. 117 | 118 | ##### `emitter.on('error', fn)` 119 | 120 | If `errorHandler` was `fasle`, this will get triggered on bundle errors. If an error handler is being used, this will not get triggered. 121 | 122 | ##### `emitter.on('bundle-error', fn)` 123 | 124 | This will get triggered on bundle errors, regardless of whether `errorHandler` is being used. This can be used to respond to syntax errors, such as showing a stylized notification. 125 | 126 | ##### `emitter.close()` 127 | 128 | Closes the `watchify` instance and stops file watching. 129 | 130 | #### `version = watchifyMiddleware.getWatchifyVersion()` 131 | 132 | Primarily useful for debugging, this will return the *actual* version number of the `watchify` module being used by `watchify-middleware`. 133 | 134 | ## running the demo 135 | 136 | To run the example, first git clone and install dependencies. 137 | 138 | ```sh 139 | git clone https://github.com/mattdesl/watchify-middleware.git 140 | cd watchify-middleware 141 | npm install 142 | ``` 143 | 144 | Then: 145 | 146 | ```sh 147 | npm start 148 | ``` 149 | 150 | And open [http://localhost:8000/](http://localhost:8000/). Try making changes to [example/app.js](example/app.js) and you will see timing information in console, and reloading the browser will provide the new bundle. 151 | 152 | ## License 153 | 154 | MIT, see [LICENSE.md](http://github.com/mattdesl/watchify-middleware/blob/master/LICENSE.md) for details. 155 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | var url = require('url') 2 | console.log(url.parse(window.location.href)) 3 | 4 | var file = require('fs').readFileSync(__dirname + '/../README.md', 'utf8') 5 | console.log(file) -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var watchifyMiddleware = require('../') 2 | var http = require('http') 3 | var defaultIndex = require('simple-html-index') 4 | var browserify = require('browserify') 5 | 6 | var staticUrl = 'bundle.js' 7 | var bundler = browserify('app.js', { 8 | // config for watchify 9 | cache: {}, 10 | transform: [ 'brfs' ], 11 | packageCache: {}, 12 | debug: true, 13 | basedir: __dirname 14 | }) 15 | 16 | var watcher = watchifyMiddleware.emitter(bundler, { 17 | errorHandler: true 18 | }) 19 | 20 | watcher.on('pending', function () { 21 | console.log('pending request') 22 | }) 23 | 24 | watcher.on('update', function () { 25 | console.log('update request') 26 | }) 27 | 28 | watcher.on('log', function (ev) { 29 | if (ev.elapsed) { 30 | ev.elapsed = ev.elapsed + 'ms' 31 | ev.url = staticUrl 32 | } 33 | ev.name = 'server' 34 | console.log(JSON.stringify(ev)) 35 | }) 36 | 37 | var middleware = watcher.middleware 38 | 39 | var server = http.createServer(function (req, res) { 40 | if (req.url === '/') { 41 | defaultIndex({ entry: staticUrl }).pipe(res) 42 | } else if (req.url === '/' + staticUrl) { 43 | middleware(req, res) 44 | } 45 | }) 46 | 47 | server.listen(8000, 'localhost', function () { 48 | console.log('Listening on http://localhost:8000/') 49 | }) 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var createBundler = require('./lib/bundler') 2 | 3 | module.exports = function watchifyMiddleware (browserify, opt) { 4 | var emitter = createEmitter(browserify, opt) 5 | return emitter.middleware 6 | } 7 | 8 | module.exports.emitter = createEmitter 9 | 10 | module.exports.getWatchifyVersion = function () { 11 | return require('watchify/package.json').version 12 | } 13 | 14 | function createEmitter (browserify, opt) { 15 | var bundler = createBundler(browserify, opt) 16 | var pending = false 17 | var contents = '' 18 | 19 | bundler.on('pending', function () { 20 | pending = true 21 | }) 22 | 23 | bundler.on('update', function (data) { 24 | pending = false 25 | contents = data 26 | }) 27 | 28 | bundler.middleware = function middleware (req, res) { 29 | if (pending) { 30 | bundler.emit('log', { 31 | level: 'debug', 32 | type: 'request', 33 | message: 'bundle pending' 34 | }) 35 | 36 | bundler.once('update', function () { 37 | bundler.emit('log', { 38 | level: 'debug', 39 | type: 'request', 40 | message: 'bundle ready' 41 | }) 42 | submit(req, res) 43 | }) 44 | } else { 45 | submit(req, res) 46 | } 47 | } 48 | 49 | return bundler 50 | 51 | function submit (req, res) { 52 | res.setHeader('content-type', 'application/javascript; charset=utf-8') 53 | res.setHeader('content-length', contents.length) 54 | res.statusCode = req.statusCode || 200 55 | res.end(contents) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/bundler.js: -------------------------------------------------------------------------------- 1 | var createWatchify = require('watchify') 2 | var EventEmitter = require('events').EventEmitter 3 | var debounce = require('debounce') 4 | var concat = require('concat-stream') 5 | var assign = require('object-assign') 6 | var stripAnsi = require('strip-ansi') 7 | var parseError = require('./parse-error') 8 | 9 | module.exports = bundler 10 | function bundler (browserify, opt) { 11 | opt = opt || {} 12 | var emitter = new EventEmitter() 13 | var delay = opt.delay || 0 14 | var closed = false 15 | var pending = false 16 | var time = Date.now() 17 | var updates = [] 18 | var errorHandler = opt.errorHandler 19 | if (errorHandler === true) { 20 | errorHandler = defaultErrorHandler 21 | } 22 | 23 | var watchify = createWatchify(browserify, assign({}, opt, { 24 | // we use our own debounce, so make sure watchify 25 | // ignores theirs 26 | delay: 0 27 | })) 28 | var contents = null 29 | 30 | emitter.close = function () { 31 | if (closed) return 32 | closed = true 33 | if (watchify) { 34 | // needed for watchify@3.0.0 35 | // this needs to be revisited upstream 36 | setTimeout(function () { 37 | watchify.close() 38 | }, 200) 39 | } 40 | } 41 | 42 | var bundleDebounced = debounce(bundle, delay) 43 | watchify.on('update', function (rows) { 44 | if (closed) return 45 | updates = rows 46 | pending = true 47 | time = Date.now() 48 | emitter.emit('pending', updates) 49 | bundleDebounced() 50 | }) 51 | 52 | emitter.bundle = function () { 53 | if (closed) return 54 | time = Date.now() 55 | if (!pending) { 56 | pending = true 57 | process.nextTick(function () { 58 | emitter.emit('pending', updates) 59 | }) 60 | } 61 | bundle() 62 | } 63 | 64 | // initial bundle 65 | if (opt.initialBundle !== false) { 66 | emitter.bundle() 67 | } 68 | 69 | return emitter 70 | 71 | function bundle () { 72 | if (closed) { 73 | update() 74 | return 75 | } 76 | 77 | var didError = false 78 | var outStream = concat(function (body) { 79 | if (!didError) { 80 | contents = body 81 | 82 | var delay = Date.now() - time 83 | emitter.emit('log', { 84 | contentLength: contents.length, 85 | elapsed: Math.round(delay), 86 | level: 'info', 87 | type: 'bundle' 88 | }) 89 | 90 | bundleEnd() 91 | } 92 | }) 93 | 94 | var wb = watchify.bundle() 95 | // it can be nice to handle errors gracefully 96 | if (typeof errorHandler === 'function') { 97 | wb.once('error', function (err) { 98 | err.message = parseError(err) 99 | contents = errorHandler(err) || '' 100 | 101 | didError = true 102 | emitter.emit('bundle-error', err) 103 | bundleEnd() 104 | }) 105 | } else { 106 | wb.once('error', function (err) { 107 | err.message = parseError(err) 108 | emitter.emit('error', err) 109 | emitter.emit('bundle-error', err) 110 | }) 111 | } 112 | wb.pipe(outStream) 113 | 114 | function bundleEnd () { 115 | update() 116 | } 117 | } 118 | 119 | function update () { 120 | if (closed) return 121 | if (pending) { 122 | pending = false 123 | emitter.emit('update', contents, updates) 124 | updates = [] 125 | } 126 | } 127 | } 128 | 129 | function defaultErrorHandler (err) { 130 | console.error('%s', err) 131 | var msg = stripAnsi(err.message) 132 | return ';console.error(' + JSON.stringify(msg) + ');' 133 | } 134 | -------------------------------------------------------------------------------- /lib/parse-error.js: -------------------------------------------------------------------------------- 1 | // parses a syntax error for pretty-printing to console 2 | module.exports = parseError 3 | function parseError (err) { 4 | if (err.codeFrame) { // babelify@6.x 5 | return [err.message, err.codeFrame].join('\n\n') 6 | } else { // babelify@5.x and browserify 7 | return err.annotated || err.message 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "watchify-middleware", 3 | "version": "1.9.1", 4 | "description": "a server for faster watchify development", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "concat-stream": "^1.5.0", 14 | "debounce": "^1.0.0", 15 | "events": "^1.0.2", 16 | "object-assign": "^4.0.1", 17 | "strip-ansi": "^3.0.0", 18 | "watchify": "^4.0.0" 19 | }, 20 | "devDependencies": { 21 | "brfs": "^1.4.1", 22 | "browserify": "^17.0.0", 23 | "faucet": "0.0.1", 24 | "garnish": "^2.3.0", 25 | "got": "^4.2.0", 26 | "minimist": "^1.2.5", 27 | "semver": "^5.0.3", 28 | "simple-html-index": "^1.0.1", 29 | "tape": "^4.2.0" 30 | }, 31 | "scripts": { 32 | "start": "node example/server.js ", 33 | "test": "node test/index.js | faucet" 34 | }, 35 | "keywords": [ 36 | "watchify", 37 | "server", 38 | "fast", 39 | "reload", 40 | "incremental", 41 | "suspend", 42 | "request", 43 | "response", 44 | "wait", 45 | "delay", 46 | "live", 47 | "browser", 48 | "browserify" 49 | ], 50 | "repository": { 51 | "type": "git", 52 | "url": "git://github.com/mattdesl/watchify-middleware.git" 53 | }, 54 | "homepage": "https://github.com/mattdesl/watchify-middleware", 55 | "bugs": { 56 | "url": "https://github.com/mattdesl/watchify-middleware/issues" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/fixture-err.js: -------------------------------------------------------------------------------- 1 | console.log(window.location.href ,)/ -------------------------------------------------------------------------------- /test/fixture-watch.js: -------------------------------------------------------------------------------- 1 | console.log("bar") -------------------------------------------------------------------------------- /test/fixture.js: -------------------------------------------------------------------------------- 1 | console.log(window.location.href) -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var watchifyMiddleware = require('../') 2 | var test = require('tape') 3 | var http = require('http') 4 | var semver = require('semver') 5 | var browserify = require('browserify') 6 | var path = require('path') 7 | var request = require('got') 8 | var fs = require('fs') 9 | var vm = require('vm') 10 | 11 | test('gets version', function(t) { 12 | t.ok(semver.valid(watchifyMiddleware.getWatchifyVersion()), 'gets watchify version') 13 | t.end() 14 | }) 15 | 16 | test('serves bundle', function(t) { 17 | t.plan(6) 18 | var staticUrl = 'bundle.js' 19 | var bundler = browserify(path.resolve(__dirname, 'fixture.js'), { 20 | cache: {}, 21 | packageCache: {}, 22 | basedir: __dirname 23 | }) 24 | bundler.bundle(function (err, expected) { 25 | if (err) return t.fail(err) 26 | var pending = true 27 | var emitter = watchifyMiddleware.emitter(bundler) 28 | var middleware = emitter.middleware 29 | var server = http.createServer(function (req, res) { 30 | if (req.url === '/' + staticUrl) { 31 | middleware(req, res) 32 | } 33 | }) 34 | 35 | emitter.on('pending', function () { 36 | pending = false 37 | t.ok(true, 'gets pending event') 38 | }) 39 | 40 | emitter.on('update', function (src, deps) { 41 | t.equal(pending, false, 'pending gets called before update') 42 | t.equal(Array.isArray(deps), true, 'gets an array of changed deps') 43 | t.equal(deps.length, 0, 'first bundle has zero changed deps') 44 | t.equal(src.toString(), expected.toString(), 'update sends bundle source') 45 | }) 46 | 47 | server.listen(8000, 'localhost', function () { 48 | request('http://localhost:8000/' + staticUrl, function (err, bundled) { 49 | server.close() 50 | emitter.close() 51 | if (err) return t.fail(err) 52 | t.equal(bundled.toString(), expected.toString(), 'bundles match') 53 | }) 54 | }) 55 | }) 56 | }) 57 | 58 | test('serves with error handler', function(t) { 59 | t.plan(2) 60 | var bundler = browserify(path.resolve(__dirname, 'fixture-err.js'), { 61 | cache: {}, 62 | packageCache: {}, 63 | basedir: __dirname 64 | }) 65 | var emitter = watchifyMiddleware.emitter(bundler, { 66 | errorHandler: function (err) { 67 | t.ok(err.message.indexOf('ParseError') >= 0, 'errorHandler gets err') 68 | return '' 69 | } 70 | }) 71 | 72 | emitter.on('error', function () { 73 | t.fail(new Error('should not emit error when errorHandler passed')) 74 | }) 75 | 76 | emitter.on('bundle-error', function (err) { 77 | t.ok(err.message.indexOf('ParseError') >= 0, 'bundle-error gets called') 78 | emitter.close() 79 | }) 80 | }) 81 | 82 | test('serves without error handler', function(t) { 83 | t.plan(2) 84 | var bundler = browserify(path.resolve(__dirname, 'fixture-err.js'), { 85 | cache: {}, 86 | packageCache: {}, 87 | basedir: __dirname 88 | }) 89 | var emitter = watchifyMiddleware.emitter(bundler) 90 | 91 | emitter.on('error', function (err) { 92 | t.ok(err.message.indexOf('ParseError') >= 0, 'error gets called') 93 | emitter.close() 94 | }) 95 | 96 | emitter.on('bundle-error', function () { 97 | t.ok(true, 'bundle-error also gets called') 98 | emitter.close() 99 | }) 100 | }) 101 | 102 | test('does watchify stuff correctly', function(t) { 103 | t.plan(2) 104 | 105 | var fixture = path.resolve(__dirname, 'fixture-watch.js') 106 | var bundler = browserify(fixture, { 107 | cache: {}, 108 | packageCache: {}, 109 | basedir: __dirname 110 | }) 111 | var emitter = watchifyMiddleware.emitter(bundler, { 112 | initialBundle: false 113 | }) 114 | var middleware = emitter.middleware 115 | var staticUrl = 'bundle.js' 116 | var uri = 'http://localhost:8000/' + staticUrl 117 | 118 | var server = http.createServer(function (req, res) { 119 | if (req.url === '/' + staticUrl) { 120 | middleware(req, res) 121 | } 122 | }) 123 | 124 | // start as "foo" 125 | // then write "bar" 126 | fs.writeFile(fixture, 'console.log("foo")', function (err) { 127 | if (err) return t.fail(err) 128 | server.listen(8000, 'localhost', startTest) 129 | }) 130 | 131 | function startTest() { 132 | runRequest(logFoo, function () { 133 | emitter.bundle() 134 | emitter.once('pending', function () { 135 | // file save event 136 | fs.writeFile(fixture, 'console.log("bar")', function (err) { 137 | if (err) return t.fail(err) 138 | runRequest(logBar, function () { 139 | server.close() 140 | emitter.close() 141 | }) 142 | }) 143 | }) 144 | emitter.once('update', function (src) { 145 | vm.runInNewContext(src, { console: { log: logBar } }); 146 | }) 147 | }) 148 | } 149 | 150 | function runRequest (logFn, cb) { 151 | request(uri, function (err, src) { 152 | if (err) return t.fail(err) 153 | vm.runInNewContext(src, { console: { log: logFn } }); 154 | cb() 155 | }) 156 | } 157 | 158 | function logFoo (msg) { 159 | t.equal(msg, 'foo') 160 | } 161 | 162 | function logBar (msg) { 163 | t.equal(msg, 'bar') 164 | } 165 | }) --------------------------------------------------------------------------------