├── .eslintrc ├── README.md ├── bin ├── browserify-patch-server └── cmd.js ├── compose.js ├── index.js ├── makePatch.js ├── makeWatcher.js ├── notify.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jasmine": true 7 | }, 8 | "ecmaFeatures": { 9 | "arrowFunctions": true, 10 | "blockBindings": true, 11 | "classes": true, 12 | "defaultParams": true, 13 | "destructuring": true, 14 | "forOf": true, 15 | "generators": false, 16 | "modules": true, 17 | "objectLiteralComputedProperties": true, 18 | "objectLiteralDuplicateProperties": false, 19 | "objectLiteralShorthandMethods": true, 20 | "objectLiteralShorthandProperties": true, 21 | "spread": true, 22 | "superInFunctions": true, 23 | "templateStrings": true, 24 | "jsx": true 25 | }, 26 | "rules": { 27 | /** 28 | * Strict mode 29 | */ 30 | // babel inserts "use strict"; for us 31 | // http://eslint.org/docs/rules/strict 32 | "strict": [2, "never"], 33 | 34 | /** 35 | * ES6 36 | */ 37 | "no-var": 0, // http://eslint.org/docs/rules/no-var 38 | 39 | /** 40 | * Variables 41 | */ 42 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 43 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 44 | "no-unused-vars": [0, { // http://eslint.org/docs/rules/no-unused-vars 45 | "vars": "local", 46 | "args": "after-used" 47 | }], 48 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 49 | 50 | /** 51 | * Possible errors 52 | */ 53 | "comma-dangle": [2, "always"], // http://eslint.org/docs/rules/comma-dangle 54 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 55 | "no-console": 0, // http://eslint.org/docs/rules/no-console 56 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 57 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 58 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 59 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 60 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case 61 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 62 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 63 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 64 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 65 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 66 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 67 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 68 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 69 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 70 | "no-reserved-keys": 0, // http://eslint.org/docs/rules/no-reserved-keys 71 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 72 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 73 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 74 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 75 | 76 | /** 77 | * Best practices 78 | */ 79 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 80 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 81 | "default-case": 2, // http://eslint.org/docs/rules/default-case 82 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 83 | "allowKeywords": true 84 | }], 85 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 86 | "guard-for-in": 2, // http://eslint.org/docs/rules/guard-for-in 87 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 88 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 89 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null 90 | "no-eval": 2, // http://eslint.org/docs/rules/no-eval 91 | "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 92 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind 93 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough 94 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 95 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 96 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 97 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 98 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 99 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 100 | "no-new": 2, // http://eslint.org/docs/rules/no-new 101 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 102 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 103 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 104 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 105 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 106 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 107 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 108 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 109 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 110 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 111 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 112 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 113 | "no-with": 2, // http://eslint.org/docs/rules/no-with 114 | "radix": 2, // http://eslint.org/docs/rules/radix 115 | "vars-on-top": 0, // http://eslint.org/docs/rules/vars-on-top 116 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 117 | "yoda": 2, // http://eslint.org/docs/rules/yoda 118 | 119 | /** 120 | * Style 121 | */ 122 | "indent": [2, 2], // http://eslint.org/docs/rules/ 123 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 124 | "1tbs", { 125 | "allowSingleLine": true 126 | }], 127 | "quotes": [ 128 | 2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes 129 | ], 130 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 131 | "properties": "never" 132 | }], 133 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 134 | "before": false, 135 | "after": true 136 | }], 137 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 138 | "eol-last": 2, // http://eslint.org/docs/rules/eol-last 139 | "func-names": 1, // http://eslint.org/docs/rules/func-names 140 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 141 | "beforeColon": false, 142 | "afterColon": true 143 | }], 144 | "new-cap": [2, { // http://eslint.org/docs/rules/new-cap 145 | "newIsCap": true 146 | }], 147 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 148 | "max": 2 149 | }], 150 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 151 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 152 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 153 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 154 | "no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func 155 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 156 | "one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 157 | "padded-blocks": 0, // http://eslint.org/docs/rules/padded-blocks 158 | "semi": [2, "always"], // http://eslint.org/docs/rules/semi 159 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 160 | "before": false, 161 | "after": true 162 | }], 163 | "space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords 164 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 165 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 166 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 167 | "space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case 168 | "spaced-line-comment": 2 // http://eslint.org/docs/rules/spaced-line-comment 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Browserify patch server 2 | Because we need HMR an analog for webpack's [`webpack-dev-server`](http://webpack.github.io/docs/webpack-dev-server.html) 3 | 4 | ## Requirements 5 | `node` > 0.10 or `io.js` > 2.0 installed 6 | 7 | ## Install 8 | ```bash 9 | npm install browserify-patch-server --save-dev 10 | ``` 11 | 12 | ## Getting started 13 | Start `browserify-patch-server`: 14 | ```bash 15 | node_modules/.bin/bfps bundles/file.js 16 | ``` 17 | or 18 | ```bash 19 | node_modules/.bin/browserify-patch-server bundles/file.js 20 | ``` 21 | With `browserify-patch-server` you can also track multiple bundles: 22 | ```bash 23 | node_modules/.bin/browserify-patch-server bundles/file.js bundles/file2.js 24 | ``` 25 | 26 | Now you need to have a client which will connect to `localhost:`. After establishing connection you'll start receiving messages: 27 | 28 | - Bundle has been **changed successfully**: 29 | ``` 30 | { 31 | "bundle": BundleName , 32 | "patch": Patch 33 | } 34 | ``` 35 | - Bundle has **syntax error**: 36 | ``` 37 | { 38 | "bundle": BundleName , 39 | "error": Error 40 | } 41 | ``` 42 | Also, once your client will connect to the server it'll receive an initial bundle(s) and message that connection has been established: 43 | ``` 44 | { 45 | "message": "Connected to browserify-patch-server", 46 | "sources": sources , 47 | } 48 | ``` 49 | 50 | ## Examples 51 | - [Live patch for React.js](https://github.com/Kureev/browserify-react-live) 52 | 53 | Feel free to submit your example! 54 | 55 | ## Configuration 56 | By default, `browserify-patch-server` use port `8081` for websocket server. If you want to change this port you need to specify `-p` or `--port` parameter: 57 | ```bash 58 | node_modules/.bin/browserify-patch-server bundles/file.js -p 8888 59 | ``` 60 | -------------------------------------------------------------------------------- /bin/browserify-patch-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./cmd'); 4 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var argv = require('yargs') 4 | .usage('Usage: $0 ') 5 | .demand(1) 6 | .example('$0 dist/bundle.js') 7 | .option('p', { 8 | alias: 'port', 9 | describe: 'Port that would be used for establishing WebSocket connection', 10 | }) 11 | .version(function getVersion() { 12 | return require('../package.json').version; 13 | }) 14 | .alias('v', 'version') 15 | .argv; 16 | 17 | require('../')(argv._, argv); 18 | -------------------------------------------------------------------------------- /compose.js: -------------------------------------------------------------------------------- 1 | module.exports = function compose() { 2 | var funcs = arguments; 3 | return function composeWrapper() { 4 | var args = arguments; 5 | for (var i = funcs.length; i-- > 0; ) { 6 | args = [funcs[i].apply(this, args), ]; 7 | } 8 | return args[0]; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const compose = require('./compose'); 3 | const check = require('syntax-error'); 4 | const babel = require('babel'); 5 | 6 | const WebSocketServer = require('ws').Server; 7 | const Logdown = require('logdown'); 8 | const chalk = require('chalk'); 9 | const moment = require('moment'); 10 | const logger = new Logdown({ prefix: '[BDS:SYSTEM]', }); 11 | const error = new Logdown({ prefix: '[BDS:ERROR]', }); 12 | 13 | function makeBuildPatchMessage(file) { 14 | return function buildPatchMessage(patch) { 15 | return { 16 | file: file, 17 | patch: patch, 18 | }; 19 | }; 20 | } 21 | 22 | function makeUpdateSourceContent(sources) { 23 | return function updateSourceContent(path, content) { 24 | sources.forEach(function iterateFiles(source) { 25 | if (source.file === path) { 26 | source.content = content; 27 | } 28 | }); 29 | }; 30 | } 31 | 32 | function checkPatch(patch) { 33 | if (patch) { 34 | return patch; 35 | } 36 | } 37 | 38 | function getTimestamp() { 39 | return '['+ moment().format('HH:mm:ss') + ']'; 40 | } 41 | 42 | module.exports = function runServer(files, options) { 43 | const pjsonConfigPort = process.env 44 | .npm_package_browserify_patch_server_port; 45 | 46 | const port = options.port || pjsonConfigPort || 8081; 47 | 48 | const sources = files.map(function iterateFiles(file) { 49 | var buffer; 50 | try { 51 | const stats = fs.lstatSync(file); 52 | if (stats.isFile()) { 53 | buffer = babel.transformFileSync(file, { stage: 0, }); 54 | return { 55 | file: file, 56 | content: buffer.code, 57 | }; 58 | } 59 | } catch (e) { 60 | throw e; 61 | } 62 | }).filter(function filterValidSources(source) { 63 | return source; 64 | }); 65 | 66 | const updateSourceContent = makeUpdateSourceContent(sources); 67 | 68 | const wss = new WebSocketServer({ port: port, }); 69 | const broadcast = require('./notify')(wss); 70 | const patch = require('./makePatch')(sources); 71 | const watcher = require('./makeWatcher')(files); 72 | 73 | wss.on('connection', function connection(ws) { 74 | ws.send( 75 | JSON.stringify({ 76 | message: 'Connected to browserify-patch-server', 77 | sources: sources, 78 | }) 79 | ); 80 | }); 81 | 82 | watcher.on('change', function onChange(path) { 83 | const patchMessage = makeBuildPatchMessage(path); 84 | const timestamp = getTimestamp(); 85 | /** 86 | * Get latest content of the watched path 87 | */ 88 | babel.transformFile(path, { stage: 0, }, function processFile(readError, res) { 89 | if (readError) { 90 | return broadcast({ 91 | file: path, 92 | error: readError.codeFrame, 93 | }); 94 | } 95 | 96 | const _content = res.code; 97 | 98 | /** 99 | * Check for syntax errors 100 | */ 101 | const err = _content.match(/SyntaxError:/) ? _content : null; 102 | 103 | logger.info(timestamp + ' File *' + path + '* has been changed'); 104 | 105 | if (err) { 106 | const errObj = err.match(/console.error\("(.+)"\)/)[1].split(': '); 107 | const errType = errObj[0]; 108 | const errFile = errObj[1]; 109 | const errMsg = errObj[2].match(/(.+) while parsing file/)[1]; 110 | 111 | error.error(timestamp + ' Bundle *' + path + '* is corrupted:' + 112 | '\n\n ' + chalk.red(errFile + '\n') + 113 | chalk.yellow('\t ⚠ ' + errMsg) + '\n'); 114 | 115 | broadcast({ 116 | file: path, 117 | error: err, 118 | }); 119 | } else { 120 | if (wss.clients.length) { 121 | compose(broadcast, patchMessage, patch)(path, _content); 122 | logger.info(timestamp + ' Broadcasted patch for *' + 123 | path + '* to ' + wss.clients.length + ' clients'); 124 | } 125 | 126 | updateSourceContent(path, _content); 127 | } 128 | }); 129 | }); 130 | 131 | logger.info(getTimestamp() + 132 | ' *Patch server* has been started ' + 133 | 'on port *' + port + '*' 134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /makePatch.js: -------------------------------------------------------------------------------- 1 | const jsdiff = require('diff'); 2 | 3 | module.exports = function scopeOriginal(sources) { 4 | return function calculateDiff(file, changedContent) { 5 | if (!changedContent.length) return false; 6 | 7 | const fileMatches = sources.filter(function filterSources(source) { 8 | return source.file === file; 9 | }); 10 | 11 | if (!fileMatches.length) { 12 | throw Error('Directory pattern you specified to watch doesn\'t contain ' + 13 | 'any files. Please, check the pattern you specified and try again.'); 14 | } 15 | 16 | const originalContent = fileMatches[0].content; 17 | 18 | return jsdiff.createPatch(Date.now(), originalContent, changedContent); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /makeWatcher.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | const fs = require('fs'); 3 | 4 | const Logdown = require('logdown'); 5 | const moment = require('moment'); 6 | const logger = new Logdown({ prefix: '[BDS:SYSTEM]', }); 7 | 8 | module.exports = function makeWatcher(path) { 9 | return chokidar.watch(path) 10 | .on('error', function processError(err) { 11 | logger.error('Oops, an error has been occured: ' + err); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /notify.js: -------------------------------------------------------------------------------- 1 | module.exports = function createBoradcast(wss) { 2 | return function broadcast(message) { 3 | wss.clients.forEach(function each(client) { 4 | client.send(JSON.stringify(message)); 5 | }); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserify-patch-server", 3 | "version": "0.4.2", 4 | "description": "Patch server for browserify", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "bfps": "./bin/browserify-patch-server", 11 | "browserify-patch-server": "./bin/browserify-patch-server" 12 | }, 13 | "keywords": [ 14 | "browserify", 15 | "hmr" 16 | ], 17 | "author": "Alexey Kureev (https://github.com/Kureev)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "babel": "^5.8.21", 21 | "chalk": "^1.1.0", 22 | "chokidar": "^1.0.4", 23 | "diff": "^1.4.0", 24 | "logdown": "^1.2.4", 25 | "moment": "^2.10.3", 26 | "syntax-error": "^1.1.4", 27 | "ws": "^0.7.2", 28 | "yargs": "^3.15.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------