├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── bin ├── cmd.js └── server.js ├── demo └── canvas.js ├── img ├── logo-small.png └── logo-thumb.png ├── index.js ├── lib ├── fix-logs.js ├── hihat.js ├── parse-args.js ├── parse-error.js ├── prelude │ ├── console.js │ ├── node-console.js │ └── node.js ├── watchify-bundler.js └── watchify-server.js ├── package.json ├── spawn.js └── test ├── fixtures ├── browser-field │ ├── browser.js │ ├── index.js │ └── package.json ├── index.html ├── test-browser.js ├── test-electron-builtins.js ├── test-exit.js ├── test-index.js ├── test-node-browser-field.js ├── test-node-no-electron.js └── test-node-with-electron.js ├── run-tests.js └── test-exit.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | canvas.png -------------------------------------------------------------------------------- /.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 Jam3 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 | # hihat 2 | 3 | ![hihat](http://i.imgur.com/Sqpbjzl.gif) 4 | 5 | > local Node/Browser development with Chrome DevTools 6 | 7 | Runs a source file in a Chrome DevTools process. Saving the file will reload the tab. 8 | 9 | This is useful for locally unit testing browser code with the full range of Web APIs (WebGL, WebAudio, etc). It provides access to profiling, debugger statements, network requests, and so forth. 10 | 11 | It can also be used to develop typical Node projects, or as a generic [Node REPL](#repl). For example, instead of using [nodemon](https://www.npmjs.com/package/nodemon) during development, you can use `hihat` to make use of a debugger. 12 | 13 | Since it provides Browser and Node APIs, it can also be used for some simple CLI tooling, like [saving a Canvas2D to a PNG file](#save-canvas-2d-to-png-image). 14 | 15 | Under the hood, this uses [electron](https://github.com/atom/electron), [browserify](https://github.com/substack/node-browserify) and [watchify](https://github.com/substack/watchify). 16 | 17 | --- 18 | 19 | #### Update: Jan 2016 20 | 21 | A lot of new efforts are going toward [devtool](https://github.com/Jam3/devtool), a very similar project but without `browserify` and `watchify` under the hood. In many ways it replaces `hihat`, but not all. Both tools will continue to exist, although `devtool` will probably receive more regular enhancements and maintenance. 22 | 23 | ## Install 24 | 25 | [![NPM](https://nodei.co/npm/hihat.png)](https://www.npmjs.com/package/hihat) 26 | 27 | This project is currently best suited as a global install. Use `npm` to install it like so: 28 | 29 | ```sh 30 | npm install hihat -g 31 | ``` 32 | 33 | ## Basic Examples 34 | 35 | Simplest case is just to run `hihat` on any source file that can be browserified (Node/CommonJS). 36 | 37 | ```sh 38 | hihat index.js 39 | ``` 40 | 41 | Any options after `--` will be passed to browserify. For example: 42 | 43 | ```sh 44 | # transpile ES6 files 45 | hihat tests/*.js -- --transform babelify 46 | ``` 47 | 48 | You can use `--print` to redirect `console` logging into your terminal: 49 | 50 | ```sh 51 | hihat test.js --print | tap-spec 52 | ``` 53 | 54 | The process will stay open until you call `window.close()` from the client code. Also see the `--quit` and `--timeout` options in [Usage](#usage). 55 | 56 | ## Usage 57 | 58 | Usage: 59 | 60 | ```sh 61 | hihat [entries] [options] -- [browserifyOptions] 62 | ``` 63 | 64 | Options: 65 | 66 | - `--port` (default `9541`) 67 | - the port to host the local server on 68 | - `--host` (default `'localhost'`) 69 | - the host for the local development server 70 | - `--dir` (default `process.cwd()`) 71 | - the root directory to serve static files from 72 | - `--print` 73 | - `console.log` and `console.error` will print to `process.stdout` and `process.stderr` 74 | - `--quit` 75 | - uncaught errors (like syntax) will cause the application to exit (useful for unit testing) 76 | - `--frame` (default `'0,0,0,0'`) 77 | - a comma-separated string for `x,y,width,height` window bounds 78 | - if only two numbers are passed, treated as `width,height` 79 | - if `true` is passed, uses the native default size 80 | - `--no-devtool` 81 | - do not open a DevTools window when running 82 | - `--raw-output` 83 | - do not silence Chromium debug logs on stdout/stderr 84 | - `--node` 85 | - enables Node integration (see [node](#node)) 86 | - `--no-electron-builtins` 87 | - when `--node` is enabled, makes it behave more like Node by ignoring Electron builtins 88 | - `--timeout` (default 0) 89 | - a number, will close the process after this duration. Use 0 for no timeout 90 | - `--exec` 91 | - an alias for `--print`, `--no-devtool` and `--quit` options. Useful for headless executions 92 | - `--index=path/to/index.html` 93 | - optional `index.html` file to override the default (see [HTML index](#html-index)) 94 | - `--serve` 95 | - what to serve your bundle entry point as 96 | - defaults to file name if possible, otherwise 'bundle.js' 97 | - `--browser-field` 98 | - Can specify `true` or `false` to force enable/disable the `"browser"` field resolution, independently of the `--node` option 99 | 100 | By default, browserify will use source maps. You can change this with `--no-debug` as a browserify option: 101 | 102 | ```sh 103 | hihat test.js -- --no-debug 104 | ``` 105 | 106 | ## Node 107 | 108 | > **Note:** Users seeking the Node.js features may be more interested in [devtool](https://github.com/Jam3/devtool) – very similar to `hihat` but better architected to deal with large Node applications. 109 | 110 | hihat can also be used for developing *simple* Node modules. The `--node` flag will disable the `"browser"` field resolution and use actual Node modules for `process`, `Buffer`, `"os"`, etc. It also exposes `require` statement outside of the bundle, so you can use it in the Chrome Console while developing. 111 | 112 | For example, `foobar.js` 113 | 114 | ```js 115 | var fs = require('fs') 116 | 117 | fs.readdir(process.cwd(), function (err, files) { 118 | if (err) throw err 119 | debugger 120 | console.log(files) 121 | }) 122 | ``` 123 | 124 | Now we can run the following on our file: 125 | 126 | ```sh 127 | hihat foobar.js --node 128 | ``` 129 | 130 | ![screenshot](http://i.imgur.com/jZdEcxC.png) 131 | 132 | By default, enabling `--node` will also enable the Electron builtins. You can pass `--no-electron-builtins` to disable Electron modules and make the source behave more like Node. 133 | 134 | #### Limitations 135 | 136 | There are some known limitations with this approach. 137 | 138 | - Modules that use native addons (like [node-canvas](https://github.com/Automattic/node-canvas)) are not supported. 139 | - Unlike a typical Node.js program, you will need to explicitly quit the application with `window.close()` 140 | - Since the source is run through browserify, the initial build time is slow and features like `require.resolve` are not yet supported. [#21](https://github.com/Jam3/hihat/issues/21) 141 | - Some features like `process.stdin` are not possible. [#12](https://github.com/Jam3/hihat/issues/12) 142 | - Since this runs Electron instead of a plain Node.js runtime, it may produce some unusual results 143 | 144 | 145 | ## REPL 146 | 147 | If you specify `hihat` without any entry files, it will not invoke browserify or watchify. For example, you can use this as a generic alternative to the Node REPL, but with better debugging and various Web APIs. 148 | 149 | ```sh 150 | hihat --node 151 | ``` 152 | 153 | Example: 154 | 155 | ![repl](http://i.imgur.com/Xns0gGT.png) 156 | 157 | ## HTML index 158 | 159 | By default, hihat will serve a [simple HTML `index.html`](https://www.npmjs.com/package/simple-html-index) file. You can use `--index` for an alternative. The path is relative to your current working directory. 160 | 161 | ```sh 162 | hihat test.js --index=foo.html 163 | ``` 164 | 165 | And the following `foo.html`: 166 | 167 | ```html 168 | 169 | 170 | FOO 171 | 172 | 173 | 174 | 175 | 176 | 177 | ``` 178 | 179 | You can also specify a `--serve` option to force a certain entry point for your bundle. For example: 180 | 181 | ```sh 182 | hihat test.js --index=foo.html --serve=bundle.js 183 | ``` 184 | 185 | With this, your script tag would be: 186 | 187 | ```html 188 | 189 | ``` 190 | 191 | In most cases, `--serve` will default to the file name of your entry file. In complex cases, such as absolute paths or `'.'`, it may default to `'bundle.js'`. 192 | 193 | ## Advanced Examples 194 | 195 | Some more advanced uses of hihat. 196 | 197 | - [prettify TAP in console](prettify-tap-in-console) 198 | - [write clipboard to `stdout`](write-clipboard-to-stdout) 199 | - [save Canvas 2D to PNG image](save-canvas-2d-to-png-image) 200 | 201 | #### prettify TAP in console 202 | 203 | You can use the browserify plugin [tap-dev-tool](https://github.com/Jam3/tap-dev-tool) to pretty-print TAP output in the console. 204 | 205 | ```sh 206 | # install it locally 207 | npm install tap-dev-tool --save-dev 208 | 209 | # now run it as a plugin 210 | hihat test.js -- -p tap-dev-tool 211 | ``` 212 | 213 | Files that use [tap](https://www.npmjs.com/package/tap) or [tape](https://www.npmjs.com/package/tape) will be logged like so: 214 | 215 | ![tap-dev-tool](http://i.imgur.com/LS014oR.png) 216 | 217 | #### write clipboard to `stdout` 218 | 219 | Using the `clipboard` module in Electron, we can write it to stdout like so. 220 | 221 | `paste.js`: 222 | 223 | ```js 224 | var clipboard = require('clipboard') 225 | process.stdout.write(clipboard.readText() + '\n') 226 | window.close() 227 | ``` 228 | 229 | Then run: 230 | 231 | ```sh 232 | hihat paste.js --node --exec > clipboard.txt 233 | ``` 234 | 235 | This will write the clipboard contents to a new file, `clipboard.txt`. 236 | 237 | #### save Canvas 2D to PNG image 238 | 239 | Here is an example which writes a Canvas2D element into a new PNG image, using [electron-canvas-to-buffer](https://github.com/mattdesl/electron-canvas-to-buffer). 240 | 241 | `render.js` 242 | 243 | ```js 244 | var toBuffer = require('electron-canvas-to-buffer') 245 | 246 | var canvas = document.createElement('canvas') 247 | var context = canvas.getContext('2d') 248 | var width = canvas.width 249 | var height = canvas.height 250 | 251 | var gradient = context.createLinearGradient(0, 0, width, 0) 252 | gradient.addColorStop(0, '#f39821') 253 | gradient.addColorStop(1, '#f321b0') 254 | 255 | context.fillStyle = gradient 256 | context.fillRect(0, 0, width, height) 257 | 258 | process.stdout.write(toBuffer(canvas, 'image/png')) 259 | window.close() 260 | ``` 261 | 262 | Now run the following: 263 | 264 | ```sh 265 | hihat render.js --node --exec > image.png 266 | ``` 267 | 268 | And the result of `image.png` will be: 269 | 270 | ![image](http://i.imgur.com/whDkS67.png) 271 | 272 | ## See Also 273 | 274 | - [devtool](https://github.com/Jam3/devtool) - a similar tool, but built specifically for Node and without the browserify/watchify cruft 275 | 276 | ## License 277 | 278 | MIT, see [LICENSE.md](http://github.com/Jam3/hihat/blob/master/LICENSE.md) for details. 279 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var args = process.argv.slice(2) 3 | var path = require('path') 4 | var parseArgs = require('../lib/parse-args') 5 | var spawn = require('../spawn') 6 | 7 | var argv = parseArgs(args) 8 | var server = path.resolve(__dirname, 'server.js') 9 | 10 | spawn(server, args, { 11 | rawOutput: argv.rawOutput 12 | }) 13 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | require('../').fromArgs(process.argv.slice(2)) 2 | -------------------------------------------------------------------------------- /demo/canvas.js: -------------------------------------------------------------------------------- 1 | var toBuffer = require('electron-canvas-to-buffer') 2 | 3 | var canvas = document.createElement('canvas') 4 | var context = canvas.getContext('2d') 5 | var width = canvas.width 6 | var height = canvas.height 7 | 8 | var gradient = context.createLinearGradient(0, 0, width, 0) 9 | gradient.addColorStop(0, '#f39821') 10 | gradient.addColorStop(1, '#f321b0') 11 | 12 | context.fillStyle = gradient 13 | context.fillRect(0, 0, width, height) 14 | 15 | process.stdout.write(toBuffer(canvas, 'image/png')) 16 | window.close() 17 | -------------------------------------------------------------------------------- /img/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/hihat/f754bdcaa4cdb335a0e305d474a6a046e4b87143/img/logo-small.png -------------------------------------------------------------------------------- /img/logo-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Experience-Monks/hihat/f754bdcaa4cdb335a0e305d474a6a046e4b87143/img/logo-thumb.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | var path = require('path') 3 | var serializerr = require('serializerr') 4 | var getPort = require('getport') 5 | var parseArgs = require('./lib/parse-args') 6 | var defaults = require('lodash.defaults') 7 | var globby = require('globby') 8 | 9 | var createHihat = require('./lib/hihat') 10 | 11 | module.exports = hihat 12 | module.exports.fromArgs = fromArgs 13 | 14 | function fromArgs (args, opt) { 15 | var cliOpts = parseArgs(args) 16 | return hihat(assign(cliOpts, opt)) 17 | } 18 | 19 | function hihat (opts) { 20 | // require these at runtime 21 | var app = require('app') 22 | app.commandLine.appendSwitch('disable-http-cache') 23 | app.commandLine.appendSwitch('v', 0) 24 | app.commandLine.appendSwitch('vmodule', 'console=0') 25 | var BrowserWindow = require('browser-window') 26 | var globalShortcut = require('global-shortcut') 27 | 28 | opts = assign({}, opts) 29 | // ensure defaults like devtool / electron-builtins are set 30 | defaults(opts, parseArgs.defaults) 31 | 32 | var entries = opts.entries || [] 33 | if (typeof entries === 'string') { 34 | entries = [ entries ] 35 | } 36 | 37 | if (opts.exec) { 38 | opts.devtool = false 39 | opts.quit = true 40 | opts.print = true 41 | } 42 | 43 | var basedir = opts.basedir || process.cwd() 44 | 45 | Error.stackTraceLimit = Infinity 46 | 47 | // this will allow the Chrome Console to also 48 | // find modules within the current working directory 49 | if (opts.node) { 50 | var findNodeModules = require('find-node-modules') 51 | process.env.NODE_PATH = findNodeModules({ 52 | cwd: basedir, 53 | relative: false 54 | }).join(path.delimiter) 55 | } 56 | 57 | var hihat 58 | var mainWindow = null 59 | var lastError = null 60 | app.on('window-all-closed', close) 61 | 62 | process.on('uncaughtException', function (err) { 63 | process.stderr.write((err.stack ? err.stack : err) + '\n') 64 | if (opts.quit) { 65 | close() 66 | } else { 67 | lastError = err 68 | printLastError() 69 | } 70 | }) 71 | 72 | function close () { 73 | app.quit() 74 | if (hihat) { 75 | hihat.close() 76 | } 77 | } 78 | 79 | app.on('ready', function () { 80 | var basePort = opts.port || 9541 81 | getPort(basePort, function (err, port) { 82 | if (err) { 83 | console.error('Could not get available port') 84 | process.exit(1) 85 | } 86 | 87 | var unparsedArgs = opts.browserifyArgs 88 | globby(entries).then(function (entryFiles) { 89 | start(assign({}, opts, { 90 | entries: entryFiles, 91 | browserifyArgs: entryFiles.concat(unparsedArgs), 92 | port: port, 93 | host: opts.host || 'localhost', 94 | dir: opts.dir || process.cwd() 95 | })) 96 | }, function (err) { 97 | console.error('Error with glob patterns: ' + err.message) 98 | process.exit(1) 99 | }).catch(function (err) { 100 | console.error('Error running hihat: ' + err.message) 101 | process.exit(1) 102 | }) 103 | }) 104 | }) 105 | 106 | function start (opt) { 107 | hihat = createHihat(opt) 108 | .on('connect', function (ev) { 109 | var bounds = parseBounds(opts.frame) 110 | 111 | // a hidden browser window 112 | mainWindow = new BrowserWindow(assign({ 113 | 'node-integration': opts.node, 114 | 'use-content-size': true 115 | }, bounds, { 116 | preload: getPrelude(), 117 | icon: path.join(__dirname, 'img', 'logo-thumb.png') 118 | })) 119 | 120 | // reload page shortcuts 121 | setupShortcuts() 122 | 123 | var webContents = mainWindow.webContents 124 | webContents.once('did-start-loading', function () { 125 | if (opts.devtool !== false) { 126 | mainWindow.openDevTools({ detach: true }) 127 | } 128 | }) 129 | 130 | webContents.once('did-frame-finish-load', function () { 131 | mainWindow.loadURL(ev.uri) 132 | mainWindow.once('dom-ready', function () { 133 | printLastError() 134 | }) 135 | 136 | if (typeof opts.timeout === 'number') { 137 | setTimeout(function () { 138 | close() 139 | }, opts.timeout) 140 | } 141 | }) 142 | 143 | mainWindow.show() 144 | // REPL with no browserify entries 145 | if (entries.length === 0) { 146 | mainWindow.reload() 147 | } 148 | 149 | mainWindow.once('closed', function () { 150 | mainWindow = null 151 | hihat.close() 152 | }) 153 | }) 154 | .on('update', function () { 155 | if (mainWindow) { 156 | mainWindow.reload() 157 | } 158 | }) 159 | } 160 | 161 | function setupShortcuts () { 162 | app.on('browser-window-focus', function () { 163 | globalShortcut.register('CmdOrCtrl+R', refresh) 164 | globalShortcut.register('F5', refresh) 165 | }) 166 | 167 | app.on('browser-window-blur', function () { 168 | globalShortcut.unregister('CmdOrCtrl+R') 169 | globalShortcut.unregister('F5') 170 | }) 171 | } 172 | 173 | function refresh () { 174 | if (mainWindow) mainWindow.reload() 175 | } 176 | 177 | function parseBounds (frame) { 178 | var bounds = { width: 0, height: 0, x: 0, y: 0 } 179 | if (typeof frame === 'string') { 180 | var parts = frame.split(',').map(function (x) { 181 | return parseInt(x, 10) 182 | }) 183 | if (parts.length === 2) { 184 | bounds = { width: parts[0], height: parts[1]} 185 | } else if (parts.length === 4) { 186 | bounds.x = parts[0] 187 | bounds.y = parts[1] 188 | bounds.width = parts[2] 189 | bounds.height = parts[3] 190 | } else { 191 | throw new Error('must specify 2 or 4 values for --frame') 192 | } 193 | } else if (frame) { 194 | bounds = {} 195 | // allow programmatic frame object 196 | if (typeof frame.x === 'number') bounds.x = frame.x 197 | if (typeof frame.y === 'number') bounds.y = frame.y 198 | if (typeof frame.width === 'number') bounds.width = frame.width 199 | if (typeof frame.height === 'number') bounds.height = frame.height 200 | } 201 | 202 | return bounds 203 | } 204 | 205 | function printLastError () { 206 | if (!mainWindow || !lastError) return 207 | var err = serializerr(lastError) 208 | mainWindow.webContents.executeJavaScript([ 209 | '(function() {', 210 | // simulate server-side Error object 211 | 'var errObj = ' + JSON.stringify(err), 212 | 'var err = new Error()', 213 | 'mixin(err, errObj)', 214 | 'try {throw err} catch(e) {console.error(e)}', 215 | 'function mixin(a, b) { for (var key in b) a[key] = b[key] }', 216 | '})()' 217 | ].join('\n')) 218 | lastError = null 219 | } 220 | 221 | function getPrelude () { 222 | var name 223 | if (opts.node && opts.print) { 224 | name = 'node-console.js' 225 | } else if (opts.node) { 226 | name = 'node.js' 227 | } else if (opts.print) { 228 | name = 'console.js' 229 | } 230 | 231 | if (name) { 232 | return path.resolve(__dirname, 'lib', 'prelude', name) 233 | } else { 234 | return undefined 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /lib/fix-logs.js: -------------------------------------------------------------------------------- 1 | var through = require('through2') 2 | var split = require('split2') 3 | var duplexer = require('duplexer2') 4 | 5 | module.exports = function (opt) { 6 | opt = opt || {} 7 | 8 | var isMultiline = false 9 | var consoleStart = /^\[.+INFO:CONSOLE\([0-9]+\)\]\s*\"(.*)(\"|$)/ 10 | var consoleEnd = /\"\, source\: https?\:\/\// 11 | var chromiumLogs = /^\[[0-9]+[\/\:]/ 12 | 13 | var out = through() 14 | var parse = split() 15 | .on('data', function (buf) { 16 | var str = buf.toString() 17 | if (consoleStart.test(str)) { 18 | // line doesnt have an end in it 19 | if (!consoleEnd.test(str)) { 20 | isMultiline = true 21 | } 22 | } else if (isMultiline && consoleEnd.test(str)) { 23 | isMultiline = false 24 | } else if (!isMultiline && !chromiumLogs.test(str)) { 25 | out.push(str + '\n') 26 | } 27 | }) 28 | 29 | return duplexer(parse, out) 30 | } 31 | -------------------------------------------------------------------------------- /lib/hihat.js: -------------------------------------------------------------------------------- 1 | var createServer = require('./watchify-server') 2 | var createBundler = require('./watchify-bundler') 3 | var Emitter = require('events/') 4 | var once = require('once') 5 | var normalizeUrl = require('normalize-file-to-url-path') 6 | 7 | module.exports = function (opt) { 8 | opt = opt || {} 9 | if (!opt.host || !opt.port) { 10 | throw new Error('must specify port and host') 11 | } 12 | var host = opt.host 13 | var port = opt.port 14 | 15 | var emitter = new Emitter() 16 | 17 | // get the name of the script without query-string 18 | var entries = opt.entries 19 | var entry = opt.serve 20 | if (!entry && entries.length > 0) { 21 | // what to serve, defaults to file path or 'bundle.js' 22 | // in tricky situations (like '.') 23 | entry = normalizeUrl(entries[0]) || 'bundle.js' 24 | } 25 | 26 | var server = createServer({ 27 | serve: entry, 28 | entries: entries, 29 | dir: opt.dir, 30 | index: opt.index 31 | }) 32 | .on('error', function (err) { 33 | emitter.emit('error', err) 34 | }) 35 | .listen(port, host, function () { 36 | var hostname = (host || 'localhost') 37 | var uri = 'http://' + hostname + ':' + port + '/' 38 | emitter.emit('connect', { 39 | uri: uri 40 | }) 41 | }) 42 | 43 | // create a new watchify instance 44 | var bundler 45 | if (entries.length > 0) { 46 | bundler = createBundler(opt.browserifyArgs, opt) 47 | .on('update', function (contents) { 48 | server.update(contents) 49 | emitter.emit('update', entry, contents) 50 | }) 51 | .on('pending', function () { 52 | server.pending() 53 | emitter.emit('pending', entry) 54 | }) 55 | .on('error', function (err) { 56 | emitter.emit('error', err) 57 | }) 58 | } 59 | 60 | emitter.close = once(function () { 61 | server.close() 62 | if (bundler) { 63 | bundler.close() 64 | } 65 | }) 66 | 67 | return emitter 68 | } 69 | -------------------------------------------------------------------------------- /lib/parse-args.js: -------------------------------------------------------------------------------- 1 | var minimist = require('minimist') 2 | 3 | module.exports = parseArgs 4 | module.exports.defaults = { 5 | devtool: true, 6 | electronBuiltins: true, 7 | 'electron-builtins': true 8 | } 9 | 10 | function parseArgs (args) { 11 | var parsed = minimist(args, { 12 | boolean: [ 13 | 'devtool', 'quit', 'node', 14 | 'print', 'raw-output', 'exec', 15 | 'electron-builtins' 16 | ], 17 | default: module.exports.defaults, 18 | alias: { 19 | 'electron-builtins': 'electronBuiltins', 20 | 'raw-output': 'rawOutput', 21 | 'browser-field': 'browserField' 22 | }, 23 | '--': true 24 | }) 25 | parsed.browserifyArgs = parsed['--'] 26 | parsed.entries = parsed._ 27 | return parsed 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/prelude/console.js: -------------------------------------------------------------------------------- 1 | // optional --print flag 2 | // patches console to stdout/stderr 3 | var format = require('util').format 4 | var sliced = require('sliced') 5 | var isDom = require('is-dom') 6 | 7 | var nativeConsole = {} 8 | var methods = ['error', 'info', 'log', 'debug', 'warn'] 9 | 10 | var _process = require('remote').process 11 | 12 | methods.forEach(function (k) { 13 | var nativeMethod = console[k] 14 | nativeConsole[k] = nativeMethod.bind(console) 15 | 16 | console[k] = function () { 17 | var args = sliced(arguments) 18 | var isError = k === 'error' || k === 'warn' 19 | var writable = isError ? _process.stderr : _process.stdout 20 | write(writable, args) 21 | return nativeMethod.apply(this, args) 22 | } 23 | }) 24 | 25 | function write (writable, args) { 26 | var cleanArgs = args.map(function (arg) { 27 | return arg && isDom(arg) ? arg.toString() : arg 28 | }) 29 | var output = format.apply(null, cleanArgs) 30 | writable.write(output + '\n') 31 | } 32 | -------------------------------------------------------------------------------- /lib/prelude/node-console.js: -------------------------------------------------------------------------------- 1 | require('./node') 2 | require('./console') 3 | -------------------------------------------------------------------------------- /lib/prelude/node.js: -------------------------------------------------------------------------------- 1 | // patch process.argv to something more useful 2 | process.argv = require('remote').process.argv 3 | 4 | // in REPL these will be undefined to mimic Node REPL 5 | delete global.__dirname 6 | delete global.__filename 7 | -------------------------------------------------------------------------------- /lib/watchify-bundler.js: -------------------------------------------------------------------------------- 1 | var createWatchify = require('watchify') 2 | var fromArgs = require('browserify/bin/args') 3 | var Emitter = require('events/') 4 | var debounce = require('debounce') 5 | var concat = require('concat-stream') 6 | var parseError = require('./parse-error') 7 | var minimist = require('minimist') 8 | 9 | var electronBuiltins = [ 10 | 'ipc', 'remote', 'web-frame', 'clipboard', 'crash-reporter', 11 | 'native-image', 'screen', 'shell', 'electron' 12 | ] 13 | 14 | module.exports = watchifyBundler 15 | function watchifyBundler (browserifyArgs, opt) { 16 | browserifyArgs = browserifyArgs.slice() 17 | 18 | var emitter = new Emitter() 19 | var delay = opt.delay || 0 20 | var closed = false 21 | var pending = true 22 | 23 | // allow user to disable debug-by-default 24 | var useDebug = opt.debug !== false 25 | var userOpts = minimist(browserifyArgs, { 26 | boolean: 'debug', 27 | default: { debug: true }, 28 | alias: { debug: 'd' } 29 | }) 30 | if (userOpts.debug === false) { 31 | useDebug = false 32 | } 33 | 34 | var bOpts = { 35 | cache: {}, 36 | packageCache: {}, 37 | commondir: false, // needed for __dirname and __filename 38 | basedir: opt.basedir, 39 | browserField: opt.browserField, 40 | debug: useDebug 41 | } 42 | 43 | var browserField = String(opt.browserField) === 'true' 44 | // only change in non-node mode if user specifies something 45 | if (typeof opt.browserField !== 'undefined') { 46 | bOpts.browserField = browserField 47 | } 48 | 49 | // ensure browserify does not mess with our Node code 50 | if (opt.node) { 51 | // only use browser field if user wants it 52 | bOpts.browserField = browserField 53 | bOpts.builtins = false 54 | // browserify tries to use require('_process') by default 55 | // in Electron we can just insert the global process name instead 56 | bOpts.insertGlobalVars = { 57 | process: function () { return 'window.process' } 58 | } 59 | } 60 | 61 | var browserify = fromArgs(browserifyArgs, bOpts) 62 | 63 | // allow Electron builtins to work by default 64 | // when node integration is enabled 65 | if (opt.node && opt.electronBuiltins) { 66 | electronBuiltins.forEach(function (x) { 67 | browserify.exclude(x) 68 | }) 69 | } 70 | 71 | var watchify = createWatchify(browserify, { 72 | // we use our own debounce, so make sure watchify 73 | // ignores theirs 74 | delay: 0 75 | }) 76 | var contents = null 77 | 78 | emitter.close = function () { 79 | if (closed) { 80 | return 81 | } 82 | closed = true 83 | if (watchify) { 84 | // needed for watchify@3.0.0 85 | // this needs to be revisited upstream 86 | setTimeout(function () { 87 | watchify.close() 88 | }, 50) 89 | } 90 | } 91 | 92 | var bundleDebounced = debounce(bundle, delay) 93 | watchify.on('update', function () { 94 | emitter.emit('pending') 95 | pending = true 96 | bundleDebounced() 97 | }) 98 | 99 | // initial bundle 100 | emitter.emit('pending') 101 | pending = true 102 | bundle() 103 | 104 | function bundle () { 105 | if (closed) { 106 | update() 107 | return 108 | } 109 | 110 | var didError = false 111 | 112 | var outStream = concat(function (body) { 113 | if (!didError) { 114 | contents = body 115 | bundleEnd() 116 | } 117 | }) 118 | 119 | var wb = watchify.bundle() 120 | // whether we should quit on errors 121 | if (!opt.quit) { 122 | wb.once('error', function (err) { 123 | if (!opt.print) { // avoid duplication 124 | console.error('%s', err) 125 | } 126 | err = parseError(err) 127 | contents = ';console.error(' + JSON.stringify(err) + ');' 128 | didError = true 129 | bundleEnd() 130 | }) 131 | } 132 | wb.pipe(outStream) 133 | 134 | function bundleEnd () { 135 | update() 136 | } 137 | } 138 | return emitter 139 | 140 | function update () { 141 | if (pending) { 142 | pending = false 143 | emitter.emit('update', contents) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/watchify-server.js: -------------------------------------------------------------------------------- 1 | var ecstatic = require('ecstatic') 2 | var Router = require('routes-router') 3 | var http = require('http') 4 | var Emitter = require('events/') 5 | var defaultIndex = require('simple-html-index') 6 | var fs = require('fs') 7 | var path = require('path') 8 | var isAbsolute = require('path-is-absolute') 9 | 10 | module.exports = function (opts) { 11 | var handler = createHandler(opts) 12 | var server = http.createServer(handler.router) 13 | 14 | server.update = function (contents) { 15 | handler.pending = false 16 | handler.contents = contents 17 | handler.emit('update') 18 | } 19 | 20 | server.pending = function () { 21 | handler.pending = true 22 | } 23 | 24 | return server 25 | } 26 | 27 | function createHandler (opts) { 28 | var cwd = process.cwd() 29 | var basedir = opts.dir || cwd // where to serve 30 | var entries = opts.entries 31 | var staticHandler = ecstatic(basedir) 32 | var router = Router() 33 | 34 | // JS/HTML files are relative to CWD 35 | var htmlIndex = opts.index 36 | if (htmlIndex) { 37 | htmlIndex = isAbsolute(htmlIndex) 38 | ? htmlIndex 39 | : path.resolve(cwd, htmlIndex) 40 | } 41 | 42 | var emitter = new Emitter() 43 | emitter.router = router 44 | emitter.pending = false 45 | emitter.contents = '' 46 | 47 | if (entries.length > 0) { 48 | router.addRoute('/' + opts.serve, function (req, res) { 49 | if (emitter.pending) { 50 | emitter.once('update', function () { 51 | submit(req, res) 52 | }) 53 | } else { 54 | submit(req, res) 55 | } 56 | }) 57 | } 58 | 59 | router.addRoute('/index.html', generateIndex) 60 | router.addRoute('/', generateIndex) 61 | router.addRoute('*', staticHandler) 62 | 63 | return emitter 64 | 65 | function submit (req, res) { 66 | res.setHeader('content-type', 'application/javascript; charset=utf-8') 67 | res.setHeader('content-length', emitter.contents.length) 68 | res.statusCode = req.statusCode || 200 69 | 70 | res.end(emitter.contents) 71 | } 72 | 73 | function generateIndex (req, res) { 74 | res.setHeader('content-type', 'text/html; charset=utf-8') 75 | 76 | var stream 77 | if (htmlIndex) { 78 | stream = fs.createReadStream(htmlIndex) 79 | } else { 80 | stream = defaultIndex({ 81 | title: 'hihat', 82 | entry: opts.serve 83 | }) 84 | } 85 | stream.pipe(res) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hihat", 3 | "version": "2.6.4", 4 | "description": "local Node/Browser development with Chrome DevTools", 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 | "browserify": "^11.0.0", 14 | "concat-stream": "^1.4.8", 15 | "debounce": "^1.0.0", 16 | "duplexer2": "0.0.2", 17 | "ecstatic": "^0.8.0", 18 | "electron-prebuilt": "^0.36.3", 19 | "events": "^1.0.2", 20 | "find-node-modules": "^1.0.1", 21 | "getport": "^0.1.0", 22 | "globby": "~3.0.1", 23 | "is-dom": "^1.0.5", 24 | "lodash.defaults": "^3.1.2", 25 | "minimist": "^1.1.1", 26 | "normalize-file-to-url-path": "^1.0.0", 27 | "object-assign": "^3.0.0", 28 | "once": "^1.3.2", 29 | "path-is-absolute": "^1.0.0", 30 | "process": "^0.11.1", 31 | "routes-router": "^4.1.2", 32 | "serializerr": "^1.0.1", 33 | "simple-html-index": "^1.0.1", 34 | "sliced": "^1.0.1", 35 | "split2": "^1.0.0", 36 | "through2": "^0.6.5", 37 | "watchify": "^3.3.0" 38 | }, 39 | "devDependencies": { 40 | "async-each-series": "^1.0.0", 41 | "brfs": "^1.4.0", 42 | "electron-canvas-to-buffer": "^1.0.2", 43 | "standard": "^4.2.0", 44 | "tap-dev-tool": "^1.3.0", 45 | "tap-spec": "^3.0.0", 46 | "tape": "^4.0.0" 47 | }, 48 | "scripts": { 49 | "test": "standard && node test/run-tests.js", 50 | "example": "./bin/cmd.js demo/canvas.js --node --exec > canvas.png" 51 | }, 52 | "keywords": [ 53 | "chrome", 54 | "app", 55 | "dev", 56 | "tools", 57 | "nodemon", 58 | "browser", 59 | "window", 60 | "chromium", 61 | "blink", 62 | "webgl", 63 | "test", 64 | "testing", 65 | "node", 66 | "mon", 67 | "monitor", 68 | "live", 69 | "reload", 70 | "local", 71 | "development", 72 | "reloading", 73 | "hot", 74 | "fast", 75 | "rapid" 76 | ], 77 | "repository": { 78 | "type": "git", 79 | "url": "git://github.com/Jam3/hihat.git" 80 | }, 81 | "homepage": "https://github.com/Jam3/hihat", 82 | "bugs": { 83 | "url": "https://github.com/Jam3/hihat/issues" 84 | }, 85 | "bin": { 86 | "hihat": "./bin/cmd.js" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spawn.js: -------------------------------------------------------------------------------- 1 | var proc = require('child_process') 2 | var electron = require('electron-prebuilt') 3 | var logger = require('./lib/fix-logs') 4 | 5 | module.exports = spawnElectron 6 | function spawnElectron (server, args, opt) { 7 | args = args || [] 8 | opt = opt || {} 9 | 10 | // spawn electron 11 | var p = proc.spawn(electron, [ server ].concat(args)) 12 | p.stdout.pipe(process.stdout) 13 | 14 | if (opt.rawOutput) { 15 | p.stderr.pipe(process.stderr) 16 | } else { 17 | // pipe chromium-stripped logs 18 | p.stderr.pipe(logger()).pipe(process.stderr) 19 | } 20 | 21 | return p 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/browser-field/browser.js: -------------------------------------------------------------------------------- 1 | module.exports = function fromBrowser () { 2 | return 'from browser' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/browser-field/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function fromNode () { 2 | return 'from node' 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/browser-field/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browser-field", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "browser": "./browser.js", 7 | "license": "MIT", 8 | "author": { 9 | "name": "Matt DesLauriers", 10 | "email": "dave.des@gmail.com", 11 | "url": "https://github.com/mattdesl" 12 | }, 13 | "dependencies": {}, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "node test.js" 17 | }, 18 | "keywords": [], 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/mattdesl/browser-field.git" 22 | }, 23 | "homepage": "https://github.com/mattdesl/browser-field", 24 | "bugs": { 25 | "url": "https://github.com/mattdesl/browser-field/issues" 26 | } 27 | } -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | custom 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/test-browser.js: -------------------------------------------------------------------------------- 1 | // hihat test/fixtures/test-browser.js 2 | var test = require('tape') 3 | var fs = require('fs') 4 | var browserField = require('./browser-field') 5 | var path = require('path') 6 | 7 | test('should run browser code', function (t) { 8 | t.plan(5) 9 | t.equal(browserField(), 'from browser') 10 | t.equal(process.browser, true) 11 | t.equal(typeof fs.readFile, 'undefined') 12 | t.ok(document.body instanceof window.HTMLElement, 13 | 'should be an element') 14 | t.equal(path.basename(__dirname), 'fixtures', 'gets folder name') 15 | }) 16 | -------------------------------------------------------------------------------- /test/fixtures/test-electron-builtins.js: -------------------------------------------------------------------------------- 1 | var clipboard = require('clipboard') 2 | process.stdout.write(clipboard.readText() + '\n') 3 | 4 | window.close() 5 | 6 | // var clipboard = require('clipboard') 7 | 8 | // var image = clipboard.readImage() 9 | // var buffer = image.toPng() 10 | // process.stdout.write(buffer) 11 | 12 | // // close window 13 | // window.close() 14 | -------------------------------------------------------------------------------- /test/fixtures/test-exit.js: -------------------------------------------------------------------------------- 1 | setTimeout(function () { 2 | window.close() 3 | }, 500) 4 | -------------------------------------------------------------------------------- /test/fixtures/test-index.js: -------------------------------------------------------------------------------- 1 | // hihat test/fixtures/test-index.js --serve=bundle.js --index=test/fixtures/index.html 2 | var test = require('tape') 3 | 4 | test('should run index.html and serve bundle.js', function (t) { 5 | t.plan(1) 6 | t.equal(window.someGlobal, 'foobar', 'gets global') 7 | }) 8 | -------------------------------------------------------------------------------- /test/fixtures/test-node-browser-field.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | var browserField = require('./browser-field') 4 | 5 | test('should run node / electron code with browser modules', function (t) { 6 | t.plan(3) 7 | t.equal(browserField(), 'from browser', 'uses browser-field for dependencies') 8 | t.equal(process.browser, undefined, 'gets Electron process') 9 | t.equal(typeof fs.readFile, 'function', 'gets Electron fs') 10 | }) 11 | -------------------------------------------------------------------------------- /test/fixtures/test-node-no-electron.js: -------------------------------------------------------------------------------- 1 | require('native-image') 2 | // should throw an error 3 | -------------------------------------------------------------------------------- /test/fixtures/test-node-with-electron.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fs = require('fs') 3 | var nativeImage = require('native-image') 4 | var browserField = require('./browser-field') 5 | var path = require('path') 6 | 7 | test('should run node / electron code', function (t) { 8 | t.plan(7) 9 | t.equal(browserField(), 'from node', 'does not use browser-field') 10 | t.equal(process.browser, undefined, 'gets Electron process') 11 | t.equal(typeof fs.readFile, 'function', 'gets Electron fs') 12 | t.equal(path.basename(__dirname), 'fixtures', 'gets __dirname') 13 | t.equal(path.basename(__filename), 'test-node-with-electron.js', 'gets __filename') 14 | t.equal(path.basename(process.argv[2]), path.basename(__filename), 'corrected process.argv') 15 | t.ok(nativeImage, true, 'got electron builtin') 16 | }) 17 | -------------------------------------------------------------------------------- /test/run-tests.js: -------------------------------------------------------------------------------- 1 | // W.I.P. 2 | // still gotta figure out a clean way of 3 | // automating all tests into one 4 | 5 | var spawn = require('../spawn') 6 | var series = require('async-each-series') 7 | var path = require('path') 8 | var server = path.resolve(__dirname, '..', 'bin', 'server.js') 9 | 10 | function start (args, done) { 11 | return spawn(server, args).on('close', done) 12 | } 13 | 14 | function fixture (name) { 15 | return path.join(__dirname, 'fixtures', name) 16 | } 17 | 18 | function test (file) { 19 | var args = Array.prototype.slice.call(arguments, 1) 20 | return [fixture(file), '--timeout=1000', '--print', '--quit'].concat(args) 21 | } 22 | 23 | series([ 24 | test('test-node-with-electron.js', '--node'), 25 | test('test-node-browser-field.js', '--node', '--browser-field'), 26 | test('test-browser.js'), 27 | test('test-index.js', '--index=test/fixtures/index.html', '--serve=bundle.js') 28 | ], start, function () { 29 | console.log('Finished') 30 | }) 31 | -------------------------------------------------------------------------------- /test/test-exit.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var exec = require('child_process').exec 3 | var path = require('path') 4 | 5 | var cli = path.resolve(__dirname, '..', 'bin', 'cmd.js') 6 | 7 | test('should use exit code 0', function (t) { 8 | t.plan(1) 9 | var child = exec([ cli, './fixtures/test-exit.js' ].join(' '), { cwd: __dirname }) 10 | child.on('exit', function (code) { 11 | t.equal(code, 0, 'matches 0') 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------