├── .prettierignore ├── .npmignore ├── test ├── fixtures │ ├── single │ │ └── .gitignore │ └── multi-compiler │ │ └── .gitignore ├── helpers-test.js ├── realistic-test.js ├── middleware-test.js └── client-test.js ├── .gitignore ├── .mocharc.json ├── .eslintignore ├── example ├── extra.js ├── index.html ├── index-multientry.html ├── package.json ├── webpack.config.multientry.js ├── webpack.config.js ├── README.md ├── client.js └── server.js ├── helpers.js ├── .eslintrc ├── CHANGELOG.md ├── .circleci └── config.yml ├── LICENSE ├── package.json ├── client-overlay.js ├── process-update.js ├── middleware.js ├── client.js └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | test 3 | .* 4 | -------------------------------------------------------------------------------- /test/fixtures/single/.gitignore: -------------------------------------------------------------------------------- 1 | client.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage/ 3 | .nyc_output/ 4 | -------------------------------------------------------------------------------- /test/fixtures/multi-compiler/.gitignore: -------------------------------------------------------------------------------- 1 | *-compilation-client.js 2 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "spec", 3 | "exit": true 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | example/* 3 | example/node_modules/* 4 | coverage/* 5 | test/fixtures/* -------------------------------------------------------------------------------- /example/extra.js: -------------------------------------------------------------------------------- 1 | console.log('Im just a separate entry point! All alone!'); 2 | 3 | if (module.hot) { 4 | module.hot.accept(); 5 | } 6 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | var parse = require('url').parse; 2 | 3 | exports.pathMatch = function (url, path) { 4 | try { 5 | return parse(url).pathname === path; 6 | } catch (e) { 7 | return false; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true 5 | }, 6 | "plugins": ["prettier"], 7 | "rules": { 8 | "no-console": "off", 9 | "prettier/prettier": ["error"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webpack Hot Middleware Example 7 | 8 | 9 |
10 |

Date

11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/index-multientry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webpack Hot Middleware Multiple Entry Point Example 7 | 8 | 9 |
10 |

Date

11 | 17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-hot-middleware-example", 3 | "version": "1.0.0", 4 | "description": "Example of using webpack-hot-middleware", 5 | "author": "Glen Mailer ", 6 | "license": "MIT", 7 | "dependencies": { 8 | "console-stamp": "^0.2.9", 9 | "express": "^4.17.1", 10 | "morgan": "^1.10.0", 11 | "webpack": "^5.74.0", 12 | "webpack-dev-middleware": "^5.3.3", 13 | "webpack-hot-middleware": "file:.." 14 | }, 15 | "scripts": { 16 | "start:base": "WEBPACK_CONFIG=./webpack.config.js node server.js", 17 | "start:multientry": "WEBPACK_CONFIG=./webpack.config.multientry.js node server.js" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/webpack.config.multientry.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var hotMiddlewareScript = 3 | 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true'; 4 | 5 | module.exports = { 6 | mode: 'development', 7 | context: __dirname, 8 | // Include the hot middleware with each entry point 9 | entry: { 10 | // Add the client which connects to our middleware 11 | client: ['./client.js', hotMiddlewareScript], 12 | extra: ['./extra.js', hotMiddlewareScript], 13 | }, 14 | output: { 15 | path: __dirname, 16 | publicPath: '/', 17 | filename: '[name].js', 18 | }, 19 | devtool: 'source-map', 20 | plugins: [new webpack.HotModuleReplacementPlugin()], 21 | }; 22 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | context: __dirname, 6 | entry: [ 7 | // Add the client which connects to our middleware 8 | // You can use full urls like 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr' 9 | // useful if you run your app from another point like django 10 | 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000', 11 | // And then the actual application 12 | './client.js', 13 | ], 14 | output: { 15 | path: __dirname, 16 | publicPath: '/', 17 | filename: 'bundle.js', 18 | }, 19 | devtool: 'source-map', 20 | plugins: [new webpack.HotModuleReplacementPlugin()], 21 | }; 22 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Webpack Hot Middleware Example 2 | 3 | * Install deps 4 | 5 | **NOTE**: 6 | > You shoude execute `npm install` command in parent folder first. Then run this command once again in `examples/` folder. 7 | ```sh 8 | npm install 9 | ``` 10 | * Start server 11 | ```sh 12 | npm start 13 | ``` 14 | * Open page in browser http://localhost:1616 15 | * Open the developer console 16 | * Edit `client.js` & save 17 | * Watch the page reload 18 | * Also try making a syntax error in `client.js`. 19 | 20 | ## Multiple Entry Points Example 21 | 22 | There is also an example for multiple entry points in webpack. 23 | 24 | ```sh 25 | npm run start:multientry 26 | ``` 27 | 28 | * Open page in browser http://localhost:1616/multientry 29 | * Edit `client.js` or `extra.js` & save 30 | -------------------------------------------------------------------------------- /test/helpers-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var assert = require('assert'); 3 | 4 | var helpers = require('../helpers'); 5 | 6 | describe('helpers', function () { 7 | describe('pathMatch', function () { 8 | var pathMatch = helpers.pathMatch; 9 | it('should match exact path', function () { 10 | assert.ok(pathMatch('/path', '/path')); 11 | }); 12 | it('should match path with querystring', function () { 13 | assert.ok(pathMatch('/path?abc=123', '/path')); 14 | }); 15 | it('should not match different path', function () { 16 | assert.equal(pathMatch('/another', '/path'), false); 17 | }); 18 | it('should not match path with other stuff on the end', function () { 19 | assert.equal(pathMatch('/path-and', '/path'), false); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /example/client.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | var app = document.getElementById('app'); 3 | var time = document.getElementById('time'); 4 | 5 | var timer = setInterval(updateClock, 1000); 6 | 7 | function updateClock() { 8 | time.innerHTML = new Date().toString(); 9 | } 10 | 11 | // Edit these styles to see them take effect immediately 12 | app.style.display = 'table-cell'; 13 | app.style.width = '400px'; 14 | app.style.height = '400px'; 15 | app.style.border = '3px solid #339'; 16 | app.style.background = '#99d'; 17 | app.style.color = '#333'; 18 | app.style.textAlign = 'center'; 19 | app.style.verticalAlign = 'middle'; 20 | 21 | // Uncomment one of the following lines to see error handling 22 | // require('unknown-module') 23 | // } syntax-error 24 | 25 | // Uncomment this next line to trigger a warning 26 | // require('Assert') 27 | // require('assert'); 28 | 29 | if (module.hot) { 30 | module.hot.accept(); 31 | module.hot.dispose(function () { 32 | clearInterval(timer); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.26.1](https://github.com/webpack/webpack-dev-middleware/compare/v2.26.0...v2.26.1) (2024-02-01) 6 | 7 | 8 | ### Fixes 9 | 10 | * add fallbacks to `moduleName` and `loc` webpack 5 error fields if not present in middleware's error formatter 11 | 12 | ### [2.26.0](https://github.com/webpack/webpack-dev-middleware/compare/v2.25.4...v2.26.0) (2023-12-25) 13 | 14 | 15 | ### Feature 16 | 17 | * added the `statsOption` option 18 | 19 | ### [2.25.4](https://github.com/webpack/webpack-dev-middleware/compare/v2.25.3...v2.25.4) (2023-06-21) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * use reduce instead of Object.fromEntries to support old browsers 25 | 26 | ### [2.25.3](https://github.com/webpack/webpack-dev-middleware/compare/v2.25.2...v2.25.3) (2022-11-08) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * compatibility with webpack 5 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | workflow: 5 | jobs: 6 | - lint: 7 | filters: 8 | tags: 9 | only: /.*/ 10 | - test: 11 | matrix: 12 | parameters: 13 | node_version: [lts] 14 | filters: 15 | tags: 16 | only: /.*/ 17 | 18 | 19 | commands: 20 | setup: 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | keys: 25 | - npm-cache-{{ checksum "package-lock.json" }} 26 | - npm-cache- 27 | - run: npm ci 28 | 29 | jobs: 30 | lint: 31 | working_directory: /mnt/ramdisk 32 | docker: 33 | - image: cimg/node:lts 34 | steps: 35 | - setup 36 | - run: 37 | command: npm run lint 38 | when: always 39 | 40 | test: 41 | parameters: 42 | node_version: 43 | type: string 44 | docker: 45 | - image: cimg/node:<< parameters.node_version >> 46 | working_directory: /mnt/ramdisk 47 | steps: 48 | - setup 49 | - run: NODE_OPTIONS=--openssl-legacy-provider npm test 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all 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 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-hot-middleware", 3 | "version": "2.26.1", 4 | "description": "Webpack hot reloading you can attach to your own server", 5 | "keywords": [ 6 | "webpack", 7 | "hmr", 8 | "hot", 9 | "module", 10 | "reloading", 11 | "hot-reloading", 12 | "middleware", 13 | "express", 14 | "bundler" 15 | ], 16 | "main": "middleware.js", 17 | "scripts": { 18 | "test": "mocha", 19 | "coverage": "nyc npm run test", 20 | "lint": "eslint . --max-warnings 0", 21 | "prettier": "prettier --write ." 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/webpack/webpack-hot-middleware.git" 26 | }, 27 | "author": "Glen Mailer ", 28 | "license": "MIT", 29 | "dependencies": { 30 | "ansi-html-community": "0.0.8", 31 | "html-entities": "^2.1.0", 32 | "strip-ansi": "^6.0.0" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^7.19.0", 36 | "eslint-plugin-prettier": "^3.3.1", 37 | "express": "^4.17.1", 38 | "mocha": "^10.1.0", 39 | "nyc": "^15.1.0", 40 | "prettier": "^2.2.1", 41 | "sinon": "^9.2.4", 42 | "supertest": "^6.1.3", 43 | "webpack": "^5.74.0", 44 | "webpack-dev-middleware": "^5.3.3" 45 | }, 46 | "prettier": { 47 | "singleQuote": true, 48 | "trailingComma": "es5", 49 | "arrowParens": "always" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | var express = require('express'); 4 | 5 | require('console-stamp')(console, 'HH:MM:ss.l'); 6 | 7 | var app = express(); 8 | 9 | app.use(require('morgan')('short')); 10 | 11 | // ************************************ 12 | // This is the real meat of the example 13 | // ************************************ 14 | (function () { 15 | // Step 1: Create & configure a webpack compiler 16 | var webpack = require('webpack'); 17 | var webpackConfig = require(process.env.WEBPACK_CONFIG 18 | ? process.env.WEBPACK_CONFIG 19 | : './webpack.config'); 20 | var compiler = webpack(webpackConfig); 21 | 22 | // Step 2: Attach the dev middleware to the compiler & the server 23 | app.use( 24 | require('webpack-dev-middleware')(compiler, { 25 | publicPath: webpackConfig.output.publicPath, 26 | }) 27 | ); 28 | 29 | // Step 3: Attach the hot middleware to the compiler & the server 30 | app.use( 31 | require('webpack-hot-middleware')(compiler, { 32 | log: console.log, 33 | path: '/__webpack_hmr', 34 | heartbeat: 10 * 1000, 35 | }) 36 | ); 37 | })(); 38 | 39 | // Do anything you like with the rest of your express application. 40 | 41 | app.get('/', function (req, res) { 42 | res.sendFile(__dirname + '/index.html'); 43 | }); 44 | app.get('/multientry', function (req, res) { 45 | res.sendFile(__dirname + '/index-multientry.html'); 46 | }); 47 | 48 | if (require.main === module) { 49 | var server = http.createServer(app); 50 | server.listen(process.env.PORT || 1616, "localhost", function () { 51 | console.log('Listening on %j', server.address()); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /client-overlay.js: -------------------------------------------------------------------------------- 1 | /*eslint-env browser*/ 2 | 3 | var clientOverlay = document.createElement('div'); 4 | clientOverlay.id = 'webpack-hot-middleware-clientOverlay'; 5 | var styles = { 6 | background: 'rgba(0,0,0,0.85)', 7 | color: '#e8e8e8', 8 | lineHeight: '1.6', 9 | whiteSpace: 'pre', 10 | fontFamily: 'Menlo, Consolas, monospace', 11 | fontSize: '13px', 12 | position: 'fixed', 13 | zIndex: 9999, 14 | padding: '10px', 15 | left: 0, 16 | right: 0, 17 | top: 0, 18 | bottom: 0, 19 | overflow: 'auto', 20 | dir: 'ltr', 21 | textAlign: 'left', 22 | }; 23 | 24 | var ansiHTML = require('ansi-html-community'); 25 | var colors = { 26 | reset: ['transparent', 'transparent'], 27 | black: '181818', 28 | red: 'ff3348', 29 | green: '3fff4f', 30 | yellow: 'ffd30e', 31 | blue: '169be0', 32 | magenta: 'f840b7', 33 | cyan: '0ad8e9', 34 | lightgrey: 'ebe7e3', 35 | darkgrey: '6d7891', 36 | }; 37 | 38 | var htmlEntities = require('html-entities'); 39 | 40 | function showProblems(type, lines) { 41 | clientOverlay.innerHTML = ''; 42 | lines.forEach(function (msg) { 43 | msg = ansiHTML(htmlEntities.encode(msg)); 44 | var div = document.createElement('div'); 45 | div.style.marginBottom = '26px'; 46 | div.innerHTML = problemType(type) + ' in ' + msg; 47 | clientOverlay.appendChild(div); 48 | }); 49 | if (document.body) { 50 | document.body.appendChild(clientOverlay); 51 | } 52 | } 53 | 54 | function clear() { 55 | if (document.body && clientOverlay.parentNode) { 56 | document.body.removeChild(clientOverlay); 57 | } 58 | } 59 | 60 | function problemType(type) { 61 | var problemColors = { 62 | errors: colors.red, 63 | warnings: colors.yellow, 64 | }; 65 | var color = problemColors[type] || colors.red; 66 | return ( 67 | '' + 70 | type.slice(0, -1).toUpperCase() + 71 | '' 72 | ); 73 | } 74 | 75 | module.exports = function (options) { 76 | for (var color in options.ansiColors) { 77 | if (color in colors) { 78 | colors[color] = options.ansiColors[color]; 79 | } 80 | ansiHTML.setColors(colors); 81 | } 82 | 83 | for (var style in options.overlayStyles) { 84 | styles[style] = options.overlayStyles[style]; 85 | } 86 | 87 | for (var key in styles) { 88 | clientOverlay.style[key] = styles[key]; 89 | } 90 | 91 | return { 92 | showProblems: showProblems, 93 | clear: clear, 94 | }; 95 | }; 96 | 97 | module.exports.clear = clear; 98 | module.exports.showProblems = showProblems; 99 | -------------------------------------------------------------------------------- /process-update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based heavily on https://github.com/webpack/webpack/blob/ 3 | * c0afdf9c6abc1dd70707c594e473802a566f7b6e/hot/only-dev-server.js 4 | * Original copyright Tobias Koppers @sokra (MIT license) 5 | */ 6 | 7 | /* global window __webpack_hash__ */ 8 | 9 | if (!module.hot) { 10 | throw new Error('[HMR] Hot Module Replacement is disabled.'); 11 | } 12 | 13 | var hmrDocsUrl = 'https://webpack.js.org/concepts/hot-module-replacement/'; // eslint-disable-line max-len 14 | 15 | var lastHash; 16 | var failureStatuses = { abort: 1, fail: 1 }; 17 | var applyOptions = { 18 | ignoreUnaccepted: true, 19 | ignoreDeclined: true, 20 | ignoreErrored: true, 21 | onUnaccepted: function (data) { 22 | console.warn( 23 | 'Ignored an update to unaccepted module ' + data.chain.join(' -> ') 24 | ); 25 | }, 26 | onDeclined: function (data) { 27 | console.warn( 28 | 'Ignored an update to declined module ' + data.chain.join(' -> ') 29 | ); 30 | }, 31 | onErrored: function (data) { 32 | console.error(data.error); 33 | console.warn( 34 | 'Ignored an error while updating module ' + 35 | data.moduleId + 36 | ' (' + 37 | data.type + 38 | ')' 39 | ); 40 | }, 41 | }; 42 | 43 | function upToDate(hash) { 44 | if (hash) lastHash = hash; 45 | return lastHash == __webpack_hash__; 46 | } 47 | 48 | module.exports = function (hash, moduleMap, options) { 49 | var reload = options.reload; 50 | if (!upToDate(hash) && module.hot.status() == 'idle') { 51 | if (options.log) console.log('[HMR] Checking for updates on the server...'); 52 | check(); 53 | } 54 | 55 | function check() { 56 | var cb = function (err, updatedModules) { 57 | if (err) return handleError(err); 58 | 59 | if (!updatedModules) { 60 | if (options.warn) { 61 | console.warn('[HMR] Cannot find update (Full reload needed)'); 62 | console.warn('[HMR] (Probably because of restarting the server)'); 63 | } 64 | performReload(); 65 | return null; 66 | } 67 | 68 | var applyCallback = function (applyErr, renewedModules) { 69 | if (applyErr) return handleError(applyErr); 70 | 71 | if (!upToDate()) check(); 72 | 73 | logUpdates(updatedModules, renewedModules); 74 | }; 75 | 76 | var applyResult = module.hot.apply(applyOptions, applyCallback); 77 | // webpack 2 promise 78 | if (applyResult && applyResult.then) { 79 | // HotModuleReplacement.runtime.js refers to the result as `outdatedModules` 80 | applyResult.then(function (outdatedModules) { 81 | applyCallback(null, outdatedModules); 82 | }); 83 | applyResult.catch(applyCallback); 84 | } 85 | }; 86 | 87 | var result = module.hot.check(false, cb); 88 | // webpack 2 promise 89 | if (result && result.then) { 90 | result.then(function (updatedModules) { 91 | cb(null, updatedModules); 92 | }); 93 | result.catch(cb); 94 | } 95 | } 96 | 97 | function logUpdates(updatedModules, renewedModules) { 98 | var unacceptedModules = updatedModules.filter(function (moduleId) { 99 | return renewedModules && renewedModules.indexOf(moduleId) < 0; 100 | }); 101 | 102 | if (unacceptedModules.length > 0) { 103 | if (options.warn) { 104 | console.warn( 105 | "[HMR] The following modules couldn't be hot updated: " + 106 | '(Full reload needed)\n' + 107 | 'This is usually because the modules which have changed ' + 108 | '(and their parents) do not know how to hot reload themselves. ' + 109 | 'See ' + 110 | hmrDocsUrl + 111 | ' for more details.' 112 | ); 113 | unacceptedModules.forEach(function (moduleId) { 114 | console.warn('[HMR] - ' + (moduleMap[moduleId] || moduleId)); 115 | }); 116 | } 117 | performReload(); 118 | return; 119 | } 120 | 121 | if (options.log) { 122 | if (!renewedModules || renewedModules.length === 0) { 123 | console.log('[HMR] Nothing hot updated.'); 124 | } else { 125 | console.log('[HMR] Updated modules:'); 126 | renewedModules.forEach(function (moduleId) { 127 | console.log('[HMR] - ' + (moduleMap[moduleId] || moduleId)); 128 | }); 129 | } 130 | 131 | if (upToDate()) { 132 | console.log('[HMR] App is up to date.'); 133 | } 134 | } 135 | } 136 | 137 | function handleError(err) { 138 | if (module.hot.status() in failureStatuses) { 139 | if (options.warn) { 140 | console.warn('[HMR] Cannot check for update (Full reload needed)'); 141 | console.warn('[HMR] ' + (err.stack || err.message)); 142 | } 143 | performReload(); 144 | return; 145 | } 146 | if (options.warn) { 147 | console.warn('[HMR] Update check failed: ' + (err.stack || err.message)); 148 | } 149 | } 150 | 151 | function performReload() { 152 | if (reload) { 153 | if (options.warn) console.warn('[HMR] Reloading page'); 154 | window.location.reload(); 155 | } 156 | } 157 | }; 158 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = webpackHotMiddleware; 2 | 3 | var helpers = require('./helpers'); 4 | var pathMatch = helpers.pathMatch; 5 | 6 | function webpackHotMiddleware(compiler, opts) { 7 | opts = opts || {}; 8 | opts.log = 9 | typeof opts.log == 'undefined' ? console.log.bind(console) : opts.log; 10 | opts.path = opts.path || '/__webpack_hmr'; 11 | opts.heartbeat = opts.heartbeat || 10 * 1000; 12 | opts.statsOptions = 13 | typeof opts.statsOptions == 'undefined' ? {} : opts.statsOptions; 14 | 15 | var eventStream = createEventStream(opts.heartbeat); 16 | var latestStats = null; 17 | var closed = false; 18 | 19 | if (compiler.hooks) { 20 | compiler.hooks.invalid.tap('webpack-hot-middleware', onInvalid); 21 | compiler.hooks.done.tap('webpack-hot-middleware', onDone); 22 | } else { 23 | compiler.plugin('invalid', onInvalid); 24 | compiler.plugin('done', onDone); 25 | } 26 | function onInvalid() { 27 | if (closed) return; 28 | latestStats = null; 29 | if (opts.log) opts.log('webpack building...'); 30 | eventStream.publish({ action: 'building' }); 31 | } 32 | function onDone(statsResult) { 33 | if (closed) return; 34 | // Keep hold of latest stats so they can be propagated to new clients 35 | latestStats = statsResult; 36 | publishStats( 37 | 'built', 38 | latestStats, 39 | eventStream, 40 | opts.log, 41 | opts.statsOptions 42 | ); 43 | } 44 | var middleware = function (req, res, next) { 45 | if (closed) return next(); 46 | if (!pathMatch(req.url, opts.path)) return next(); 47 | eventStream.handler(req, res); 48 | if (latestStats) { 49 | // Explicitly not passing in `log` fn as we don't want to log again on 50 | // the server 51 | publishStats('sync', latestStats, eventStream, false, opts.statsOptions); 52 | } 53 | }; 54 | middleware.publish = function (payload) { 55 | if (closed) return; 56 | eventStream.publish(payload); 57 | }; 58 | middleware.close = function () { 59 | if (closed) return; 60 | // Can't remove compiler plugins, so we just set a flag and noop if closed 61 | // https://github.com/webpack/tapable/issues/32#issuecomment-350644466 62 | closed = true; 63 | eventStream.close(); 64 | eventStream = null; 65 | }; 66 | return middleware; 67 | } 68 | 69 | function createEventStream(heartbeat) { 70 | var clientId = 0; 71 | var clients = {}; 72 | function everyClient(fn) { 73 | Object.keys(clients).forEach(function (id) { 74 | fn(clients[id]); 75 | }); 76 | } 77 | var interval = setInterval(function heartbeatTick() { 78 | everyClient(function (client) { 79 | client.write('data: \uD83D\uDC93\n\n'); 80 | }); 81 | }, heartbeat).unref(); 82 | return { 83 | close: function () { 84 | clearInterval(interval); 85 | everyClient(function (client) { 86 | if (!client.finished) client.end(); 87 | }); 88 | clients = {}; 89 | }, 90 | handler: function (req, res) { 91 | var headers = { 92 | 'Access-Control-Allow-Origin': '*', 93 | 'Content-Type': 'text/event-stream;charset=utf-8', 94 | 'Cache-Control': 'no-cache, no-transform', 95 | // While behind nginx, event stream should not be buffered: 96 | // http://nginx.org/docs/http/ngx_http_proxy_module.html#proxy_buffering 97 | 'X-Accel-Buffering': 'no', 98 | }; 99 | 100 | var isHttp1 = !(parseInt(req.httpVersion) >= 2); 101 | if (isHttp1) { 102 | req.socket.setKeepAlive(true); 103 | Object.assign(headers, { 104 | Connection: 'keep-alive', 105 | }); 106 | } 107 | 108 | res.writeHead(200, headers); 109 | res.write('\n'); 110 | var id = clientId++; 111 | clients[id] = res; 112 | req.on('close', function () { 113 | if (!res.finished) res.end(); 114 | delete clients[id]; 115 | }); 116 | }, 117 | publish: function (payload) { 118 | everyClient(function (client) { 119 | client.write('data: ' + JSON.stringify(payload) + '\n\n'); 120 | }); 121 | }, 122 | }; 123 | } 124 | 125 | function publishStats(action, statsResult, eventStream, log, statsOptions) { 126 | var resultStatsOptions = Object.assign( 127 | { 128 | all: false, 129 | cached: true, 130 | children: true, 131 | modules: true, 132 | timings: true, 133 | hash: true, 134 | errors: true, 135 | warnings: true, 136 | }, 137 | statsOptions 138 | ); 139 | 140 | var bundles = []; 141 | 142 | // multi-compiler stats have stats for each child compiler 143 | // see https://github.com/webpack/webpack/blob/main/lib/MultiCompiler.js#L97 144 | if (statsResult.stats) { 145 | var processed = statsResult.stats.map(function (stats) { 146 | return extractBundles(normalizeStats(stats, resultStatsOptions)); 147 | }); 148 | 149 | bundles = processed.flat(); 150 | } else { 151 | bundles = extractBundles(normalizeStats(statsResult, resultStatsOptions)); 152 | } 153 | 154 | bundles.forEach(function (stats) { 155 | var name = stats.name || ''; 156 | 157 | // Fallback to compilation name in case of 1 bundle (if it exists) 158 | if (!name && stats.compilation) { 159 | name = stats.compilation.name || ''; 160 | } 161 | 162 | if (log) { 163 | log( 164 | 'webpack built ' + 165 | (name ? name + ' ' : '') + 166 | stats.hash + 167 | ' in ' + 168 | stats.time + 169 | 'ms' 170 | ); 171 | } 172 | 173 | eventStream.publish({ 174 | name: name, 175 | action: action, 176 | time: stats.time, 177 | hash: stats.hash, 178 | warnings: formatErrors(stats.warnings || []), 179 | errors: formatErrors(stats.errors || []), 180 | modules: buildModuleMap(stats.modules), 181 | }); 182 | }); 183 | } 184 | 185 | function formatErrors(errors) { 186 | if (!errors || !errors.length) { 187 | return []; 188 | } 189 | 190 | if (typeof errors[0] === 'string') { 191 | return errors; 192 | } 193 | 194 | // Convert webpack@5 error info into a backwards-compatible flat string 195 | return errors.map(function (error) { 196 | var moduleName = error.moduleName || ''; 197 | var loc = error.loc || ''; 198 | return moduleName + ' ' + loc + '\n' + error.message; 199 | }); 200 | } 201 | 202 | function normalizeStats(stats, statsOptions) { 203 | var statsJson = stats.toJson(statsOptions); 204 | 205 | if (stats.compilation) { 206 | // webpack 5 has the compilation property directly on stats object 207 | Object.assign(statsJson, { 208 | compilation: stats.compilation, 209 | }); 210 | } 211 | 212 | return statsJson; 213 | } 214 | 215 | function extractBundles(stats) { 216 | // Stats has modules, single bundle 217 | if (stats.modules) return [stats]; 218 | 219 | // Stats has children, multiple bundles 220 | if (stats.children && stats.children.length) return stats.children; 221 | 222 | // Not sure, assume single 223 | return [stats]; 224 | } 225 | 226 | function buildModuleMap(modules) { 227 | var map = {}; 228 | modules.forEach(function (module) { 229 | map[module.id] = module.name; 230 | }); 231 | return map; 232 | } 233 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | /*eslint-env browser*/ 2 | /*global __resourceQuery __webpack_public_path__*/ 3 | 4 | var options = { 5 | path: '/__webpack_hmr', 6 | timeout: 20 * 1000, 7 | overlay: true, 8 | reload: false, 9 | log: true, 10 | warn: true, 11 | name: '', 12 | autoConnect: true, 13 | overlayStyles: {}, 14 | overlayWarnings: false, 15 | ansiColors: {}, 16 | }; 17 | if (__resourceQuery) { 18 | var params = Array.from(new URLSearchParams(__resourceQuery.slice(1))); 19 | var overrides = params.reduce(function (memo, param) { 20 | memo[param[0]] = param[1]; 21 | return memo; 22 | }, {}); 23 | 24 | setOverrides(overrides); 25 | } 26 | 27 | if (typeof window === 'undefined') { 28 | // do nothing 29 | } else if (typeof window.EventSource === 'undefined') { 30 | console.warn( 31 | "webpack-hot-middleware's client requires EventSource to work. " + 32 | 'You should include a polyfill if you want to support this browser: ' + 33 | 'https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events#Tools' 34 | ); 35 | } else { 36 | if (options.autoConnect) { 37 | connect(); 38 | } 39 | } 40 | 41 | /* istanbul ignore next */ 42 | function setOptionsAndConnect(overrides) { 43 | setOverrides(overrides); 44 | connect(); 45 | } 46 | 47 | function setOverrides(overrides) { 48 | if (overrides.autoConnect) 49 | options.autoConnect = overrides.autoConnect == 'true'; 50 | if (overrides.path) options.path = overrides.path; 51 | if (overrides.timeout) options.timeout = overrides.timeout; 52 | if (overrides.overlay) options.overlay = overrides.overlay !== 'false'; 53 | if (overrides.reload) options.reload = overrides.reload !== 'false'; 54 | if (overrides.noInfo && overrides.noInfo !== 'false') { 55 | options.log = false; 56 | } 57 | if (overrides.name) { 58 | options.name = overrides.name; 59 | } 60 | if (overrides.quiet && overrides.quiet !== 'false') { 61 | options.log = false; 62 | options.warn = false; 63 | } 64 | 65 | if (overrides.dynamicPublicPath) { 66 | options.path = __webpack_public_path__ + options.path; 67 | } 68 | 69 | if (overrides.ansiColors) 70 | options.ansiColors = JSON.parse(overrides.ansiColors); 71 | if (overrides.overlayStyles) 72 | options.overlayStyles = JSON.parse(overrides.overlayStyles); 73 | 74 | if (overrides.overlayWarnings) { 75 | options.overlayWarnings = overrides.overlayWarnings == 'true'; 76 | } 77 | } 78 | 79 | function EventSourceWrapper() { 80 | var source; 81 | var lastActivity = new Date(); 82 | var listeners = []; 83 | 84 | init(); 85 | var timer = setInterval(function () { 86 | if (new Date() - lastActivity > options.timeout) { 87 | handleDisconnect(); 88 | } 89 | }, options.timeout / 2); 90 | 91 | function init() { 92 | source = new window.EventSource(options.path); 93 | source.onopen = handleOnline; 94 | source.onerror = handleDisconnect; 95 | source.onmessage = handleMessage; 96 | } 97 | 98 | function handleOnline() { 99 | if (options.log) console.log('[HMR] connected'); 100 | lastActivity = new Date(); 101 | } 102 | 103 | function handleMessage(event) { 104 | lastActivity = new Date(); 105 | for (var i = 0; i < listeners.length; i++) { 106 | listeners[i](event); 107 | } 108 | } 109 | 110 | function handleDisconnect() { 111 | clearInterval(timer); 112 | source.close(); 113 | setTimeout(init, options.timeout); 114 | } 115 | 116 | return { 117 | addMessageListener: function (fn) { 118 | listeners.push(fn); 119 | }, 120 | }; 121 | } 122 | 123 | function getEventSourceWrapper() { 124 | if (!window.__whmEventSourceWrapper) { 125 | window.__whmEventSourceWrapper = {}; 126 | } 127 | if (!window.__whmEventSourceWrapper[options.path]) { 128 | // cache the wrapper for other entries loaded on 129 | // the same page with the same options.path 130 | window.__whmEventSourceWrapper[options.path] = EventSourceWrapper(); 131 | } 132 | return window.__whmEventSourceWrapper[options.path]; 133 | } 134 | 135 | function connect() { 136 | getEventSourceWrapper().addMessageListener(handleMessage); 137 | 138 | function handleMessage(event) { 139 | if (event.data == '\uD83D\uDC93') { 140 | return; 141 | } 142 | try { 143 | processMessage(JSON.parse(event.data)); 144 | } catch (ex) { 145 | if (options.warn) { 146 | console.warn('Invalid HMR message: ' + event.data + '\n' + ex); 147 | } 148 | } 149 | } 150 | } 151 | 152 | // the reporter needs to be a singleton on the page 153 | // in case the client is being used by multiple bundles 154 | // we only want to report once. 155 | // all the errors will go to all clients 156 | var singletonKey = '__webpack_hot_middleware_reporter__'; 157 | var reporter; 158 | if (typeof window !== 'undefined') { 159 | if (!window[singletonKey]) { 160 | window[singletonKey] = createReporter(); 161 | } 162 | reporter = window[singletonKey]; 163 | } 164 | 165 | function createReporter() { 166 | var strip = require('strip-ansi'); 167 | 168 | var overlay; 169 | if (typeof document !== 'undefined' && options.overlay) { 170 | overlay = require('./client-overlay')({ 171 | ansiColors: options.ansiColors, 172 | overlayStyles: options.overlayStyles, 173 | }); 174 | } 175 | 176 | var styles = { 177 | errors: 'color: #ff0000;', 178 | warnings: 'color: #999933;', 179 | }; 180 | var previousProblems = null; 181 | function log(type, obj) { 182 | var newProblems = obj[type] 183 | .map(function (msg) { 184 | return strip(msg); 185 | }) 186 | .join('\n'); 187 | if (previousProblems == newProblems) { 188 | return; 189 | } else { 190 | previousProblems = newProblems; 191 | } 192 | 193 | var style = styles[type]; 194 | var name = obj.name ? "'" + obj.name + "' " : ''; 195 | var title = '[HMR] bundle ' + name + 'has ' + obj[type].length + ' ' + type; 196 | // NOTE: console.warn or console.error will print the stack trace 197 | // which isn't helpful here, so using console.log to escape it. 198 | if (console.group && console.groupEnd) { 199 | console.group('%c' + title, style); 200 | console.log('%c' + newProblems, style); 201 | console.groupEnd(); 202 | } else { 203 | console.log( 204 | '%c' + title + '\n\t%c' + newProblems.replace(/\n/g, '\n\t'), 205 | style + 'font-weight: bold;', 206 | style + 'font-weight: normal;' 207 | ); 208 | } 209 | } 210 | 211 | return { 212 | cleanProblemsCache: function () { 213 | previousProblems = null; 214 | }, 215 | problems: function (type, obj) { 216 | if (options.warn) { 217 | log(type, obj); 218 | } 219 | if (overlay) { 220 | if (options.overlayWarnings || type === 'errors') { 221 | overlay.showProblems(type, obj[type]); 222 | return false; 223 | } 224 | overlay.clear(); 225 | } 226 | return true; 227 | }, 228 | success: function () { 229 | if (overlay) overlay.clear(); 230 | }, 231 | useCustomOverlay: function (customOverlay) { 232 | overlay = customOverlay; 233 | }, 234 | }; 235 | } 236 | 237 | var processUpdate = require('./process-update'); 238 | 239 | var customHandler; 240 | var subscribeAllHandler; 241 | function processMessage(obj) { 242 | switch (obj.action) { 243 | case 'building': 244 | if (options.log) { 245 | console.log( 246 | '[HMR] bundle ' + 247 | (obj.name ? "'" + obj.name + "' " : '') + 248 | 'rebuilding' 249 | ); 250 | } 251 | break; 252 | case 'built': 253 | if (options.log) { 254 | console.log( 255 | '[HMR] bundle ' + 256 | (obj.name ? "'" + obj.name + "' " : '') + 257 | 'rebuilt in ' + 258 | obj.time + 259 | 'ms' 260 | ); 261 | } 262 | // fall through 263 | case 'sync': 264 | if (obj.name && options.name && obj.name !== options.name) { 265 | return; 266 | } 267 | var applyUpdate = true; 268 | if (obj.errors.length > 0) { 269 | if (reporter) reporter.problems('errors', obj); 270 | applyUpdate = false; 271 | } else if (obj.warnings.length > 0) { 272 | if (reporter) { 273 | var overlayShown = reporter.problems('warnings', obj); 274 | applyUpdate = overlayShown; 275 | } 276 | } else { 277 | if (reporter) { 278 | reporter.cleanProblemsCache(); 279 | reporter.success(); 280 | } 281 | } 282 | if (applyUpdate) { 283 | processUpdate(obj.hash, obj.modules, options); 284 | } 285 | break; 286 | default: 287 | if (customHandler) { 288 | customHandler(obj); 289 | } 290 | } 291 | 292 | if (subscribeAllHandler) { 293 | subscribeAllHandler(obj); 294 | } 295 | } 296 | 297 | if (module) { 298 | module.exports = { 299 | subscribeAll: function subscribeAll(handler) { 300 | subscribeAllHandler = handler; 301 | }, 302 | subscribe: function subscribe(handler) { 303 | customHandler = handler; 304 | }, 305 | useCustomOverlay: function useCustomOverlay(customOverlay) { 306 | if (reporter) reporter.useCustomOverlay(customOverlay); 307 | }, 308 | setOptionsAndConnect: setOptionsAndConnect, 309 | }; 310 | } 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Webpack Hot Middleware 2 | 3 | Webpack hot reloading using only [webpack-dev-middleware](https://webpack.js.org/guides/development/#webpack-dev-middleware). This allows you to add hot reloading into an existing server without [webpack-dev-server](https://webpack.js.org/configuration/dev-server/). 4 | 5 | This module is **only** concerned with the mechanisms to connect a browser client to a webpack server & receive updates. It will subscribe to changes from the server and execute those changes using [webpack's HMR API](https://webpack.js.org/concepts/hot-module-replacement/). Actually making your application capable of using hot reloading to make seamless changes is out of scope, and usually handled by another library. 6 | 7 | If you're using React then some common options are [react-transform-hmr](https://github.com/gaearon/react-transform-hmr/) and [react-hot-loader](https://github.com/gaearon/react-hot-loader). 8 | 9 | [![npm version](https://img.shields.io/npm/v/webpack-hot-middleware.svg)](https://www.npmjs.com/package/webpack-hot-middleware) [![CircleCI](https://circleci.com/gh/webpack/webpack-hot-middleware/tree/main.svg?style=svg)](https://circleci.com/gh/webpack/webpack-hot-middleware/tree/main)[![codecov](https://codecov.io/gh/webpack/webpack-hot-middleware/branch/main/graph/badge.svg)](https://codecov.io/gh/webpack/webpack-hot-middleware)![MIT Licensed](https://img.shields.io/npm/l/webpack-hot-middleware.svg) 10 | 11 | ## Installation & Usage 12 | 13 | See [example/](./example/) for an example of usage. 14 | 15 | First, install the npm module. 16 | 17 | ```sh 18 | npm install --save-dev webpack-hot-middleware 19 | ``` 20 | 21 | Next, enable hot reloading in your webpack config: 22 | 23 | 1. Add the following plugins to the `plugins` array: 24 | ```js 25 | plugins: [ 26 | new webpack.HotModuleReplacementPlugin(), 27 | ] 28 | ``` 29 | 30 | Occurrence ensures consistent build hashes, hot module replacement is 31 | somewhat self-explanatory, no errors is used to handle errors more cleanly. 32 | 33 | 3. Add `'webpack-hot-middleware/client'` into an array of the `entry` 34 | object. For example: 35 | ```js 36 | entry: { 37 | main: ['webpack-hot-middleware/client', './src/main.js'] 38 | } 39 | ``` 40 | This connects to the server to receive notifications when the bundle 41 | rebuilds and then updates your client bundle accordingly. 42 | 43 | Now add the middleware into your server: 44 | 45 | 1. Add `webpack-dev-middleware` the usual way 46 | ```js 47 | var webpack = require('webpack'); 48 | var webpackConfig = require('./webpack.config'); 49 | var compiler = webpack(webpackConfig); 50 | 51 | app.use(require("webpack-dev-middleware")(compiler, { 52 | /* Options */ 53 | })); 54 | ``` 55 | 56 | 2. Add `webpack-hot-middleware` attached to the same compiler instance 57 | ```js 58 | app.use(require("webpack-hot-middleware")(compiler)); 59 | ``` 60 | 61 | And you're all set! 62 | 63 | ## Changelog 64 | 65 | ### 2.0.0 66 | 67 | **Breaking Change** 68 | 69 | As of version 2.0.0, all client functionality has been rolled into this module. This means that you should remove any reference to `webpack/hot/dev-server` or `webpack/hot/only-dev-server` from your webpack config. Instead, use the `reload` config option to control this behaviour. 70 | 71 | This was done to allow full control over the client receiving updates, which is now able to output full module names in the console when applying changes. 72 | 73 | ## Documentation 74 | 75 | More to come soon, you'll have to mostly rely on the example for now. 76 | 77 | ### Config 78 | 79 | #### Client 80 | 81 | Configuration options can be passed to the client by adding querystring parameters to the path in the webpack config. 82 | 83 | ```js 84 | 'webpack-hot-middleware/client?path=/__what&timeout=2000&overlay=false' 85 | ``` 86 | 87 | * **path** - The path which the middleware is serving the event stream on 88 | * **name** - Bundle name, specifically for multi-compiler mode 89 | * **timeout** - The time to wait after a disconnection before attempting to reconnect 90 | * **overlay** - Set to `false` to disable the DOM-based client-side overlay. 91 | * **reload** - Set to `true` to auto-reload the page when webpack gets stuck. 92 | * **noInfo** - Set to `true` to disable informational console logging. 93 | * **quiet** - Set to `true` to disable all console logging. 94 | * **dynamicPublicPath** - Set to `true` to use webpack `publicPath` as prefix of `path`. (We can set `__webpack_public_path__` dynamically at runtime in the entry point, see note of [output.publicPath](https://webpack.js.org/configuration/output/#output-publicpath)) 95 | * **autoConnect** - Set to `false` to use to prevent a connection being automatically opened from the client to the webpack back-end - ideal if you need to modify the options using the `setOptionsAndConnect` function 96 | * **ansiColors** - An object to customize the client overlay colors as mentioned in the [ansi-html-community](https://github.com/mahdyar/ansi-html-community#set-colors) package. 97 | * **overlayStyles** - An object to let you override or add new inline styles to the client overlay div. 98 | * **overlayWarnings** - Set to `true` to enable client overlay on warnings in addition to errors. 99 | * **statsOptions** - An object to customize stats options. 100 | 101 | > Note: 102 | > Since the `ansiColors` and `overlayStyles` options are passed via query string, you'll need to uri encode your stringified options like below: 103 | 104 | ```js 105 | var ansiColors = { 106 | red: '00FF00' // note the lack of "#" 107 | }; 108 | var overlayStyles = { 109 | color: '#FF0000' // note the inclusion of "#" (these options would be the equivalent of div.style[option] = value) 110 | }; 111 | var hotMiddlewareScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true&ansiColors=' + encodeURIComponent(JSON.stringify(ansiColors)) + '&overlayStyles=' + encodeURIComponent(JSON.stringify(overlayStyles)); 112 | ``` 113 | 114 | #### Middleware 115 | 116 | Configuration options can be passed to the middleware by passing a second argument. 117 | 118 | ```js 119 | app.use(require("webpack-hot-middleware")(compiler, { 120 | log: false, 121 | path: "/__what", 122 | heartbeat: 2000 123 | })); 124 | ``` 125 | 126 | * **log** - A function used to log lines, pass `false` to disable. Defaults to `console.log` 127 | * **path** - The path which the middleware will serve the event stream on, must match the client setting 128 | * **heartbeat** - How often to send heartbeat updates to the client to keep the connection alive. Should be less than the client's `timeout` setting - usually set to half its value. 129 | 130 | ## How it Works 131 | 132 | The middleware installs itself as a webpack plugin, and listens for compiler events. 133 | 134 | Each connected client gets a [Server Sent Events](http://www.html5rocks.com/en/tutorials/eventsource/basics/) connection, the server will publish notifications to connected clients on compiler events. 135 | 136 | When the client receives a message, it will check to see if the local code is up to date. If it isn't up to date, it will trigger webpack hot module reloading. 137 | 138 | ### Multi-compiler mode 139 | 140 | If you're using multi-compiler mode (exporting an array of config in `webpack.config.js`), set `name` parameters to make sure bundles don't process each other's updates. For example: 141 | 142 | ``` 143 | // webpack.config.js 144 | module.exports = [ 145 | { 146 | name: 'mobile', 147 | entry: { 148 | vendor: 'vendor.js', 149 | main: ['webpack-hot-middleware/client?name=mobile', 'mobile.js'] 150 | } 151 | }, 152 | { 153 | name: 'desktop', 154 | entry: { 155 | vendor: 'vendor.js', 156 | main: ['webpack-hot-middleware/client?name=desktop', 'desktop.js'] 157 | } 158 | } 159 | ] 160 | ``` 161 | 162 | ## Other Frameworks 163 | 164 | ### Hapi 165 | 166 | Use the [hapi-webpack-plugin](https://www.npmjs.com/package/hapi-webpack-plugin). 167 | 168 | ### Koa 169 | 170 | [koa-webpack-middleware](https://www.npmjs.com/package/koa-webpack-middleware) 171 | wraps this module for use with Koa 1.x 172 | 173 | [koa-webpack](https://www.npmjs.com/package/koa-webpack) 174 | can be used for Koa 2.x 175 | 176 | ## Troubleshooting 177 | 178 | ### Use on browsers without EventSource 179 | 180 | If you want to use this module with browsers that don't support eventsource, you'll need to use a [polyfill](https://libraries.io/search?platforms=NPM&q=eventsource+polyfill). See [issue #11](https://github.com/webpack/webpack-hot-middleware/issues/11) 181 | 182 | ### Not receiving updates in client when using Gzip 183 | 184 | This is because gzip generally buffers the response, but the Server Sent Events event-stream expects to be able to send data to the client immediately. You should make sure gzipping isn't being applied to the event-stream. See [issue #10](https://github.com/webpack/webpack-hot-middleware/issues/10). 185 | 186 | ### Use with auto-restarting servers 187 | 188 | This module expects to remain running while you make changes to your webpack bundle, if you use a process manager like nodemon then you will likely see very slow changes on the client side. If you want to reload the server component, either use a separate process, or find a way to reload your server routes without restarting the whole process. See https://github.com/glenjamin/ultimate-hot-reloading-example for an example of one way to do this. 189 | 190 | ### Use with multiple entry points in webpack 191 | 192 | If you want to use [multiple entry points in your webpack config](https://webpack.js.org/concepts/output/#multiple-entry-points) you need to include the hot middleware client in each entry point. This ensures that each entry point file knows how to handle hot updates. See the [examples folder README](example/README.md) for an example. 193 | 194 | ```js 195 | entry: { 196 | vendor: ['jquery', 'webpack-hot-middleware/client'], 197 | index: ['./src/index', 'webpack-hot-middleware/client'] 198 | } 199 | ``` 200 | 201 | ## License 202 | 203 | See [LICENSE file](LICENSE). 204 | -------------------------------------------------------------------------------- /test/realistic-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var path = require('path'); 3 | var qs = require('querystring'); 4 | 5 | var express = require('express'); 6 | var webpack = require('webpack'); 7 | var webpackDevMiddleware = require('webpack-dev-middleware'); 8 | 9 | var assert = require('assert'); 10 | var supertest = require('supertest'); 11 | 12 | var webpackHotMiddleware = require('../middleware'); 13 | 14 | var app; 15 | 16 | describe('realistic single compiler', function () { 17 | var compiler; 18 | var clientCode = path.resolve(__dirname, './fixtures/single/client.js'); 19 | before(function () { 20 | require('fs').writeFileSync(clientCode, 'var a = ' + Math.random() + ';\n'); 21 | 22 | compiler = webpack({ 23 | mode: 'development', 24 | entry: [ 25 | require.resolve('./fixtures/single/client.js'), 26 | require.resolve('../client.js'), 27 | ], 28 | plugins: [new webpack.HotModuleReplacementPlugin()], 29 | }); 30 | 31 | app = express(); 32 | app.use( 33 | webpackDevMiddleware(compiler, { 34 | publicPath: '/', 35 | // stats: 'none', 36 | }) 37 | ); 38 | app.use( 39 | webpackHotMiddleware(compiler, { 40 | log: function () {}, 41 | }) 42 | ); 43 | }); 44 | 45 | it('should create eventStream on /__webpack_hmr', function (done) { 46 | request('/__webpack_hmr') 47 | .expect('Content-Type', /^text\/event-stream\b/) 48 | .end(done); 49 | }); 50 | 51 | describe('first build', function () { 52 | it('should publish sync event', function (done) { 53 | request('/__webpack_hmr') 54 | .expect('Content-Type', /^text\/event-stream\b/) 55 | .end(function (err, res) { 56 | if (err) return done(err); 57 | 58 | var event = JSON.parse(res.events[0].substring(5)); 59 | 60 | assert.equal(event.action, 'sync'); 61 | assert.equal(event.name, ''); 62 | assert.ok(event.hash); 63 | assert.ok(event.time); 64 | assert.ok(Array.isArray(event.warnings)); 65 | assert.ok(Array.isArray(event.errors)); 66 | assert.ok(typeof event.modules === 'object'); 67 | 68 | done(); 69 | }); 70 | }); 71 | }); 72 | describe('after file change', function () { 73 | var res; 74 | before(function (done) { 75 | request('/__webpack_hmr') 76 | .expect('Content-Type', /^text\/event-stream\b/) 77 | .end(function (err, _res) { 78 | if (err) return done(err); 79 | 80 | res = _res; 81 | 82 | require('fs').writeFile( 83 | clientCode, 84 | 'var a = ' + Math.random() + ';\n', 85 | done 86 | ); 87 | }); 88 | }); 89 | it('should publish building event', function (done) { 90 | waitUntil( 91 | function () { 92 | return res.events.length >= 2; 93 | }, 94 | function () { 95 | var event = JSON.parse(res.events[1].substring(5)); 96 | 97 | assert.equal(event.action, 'building'); 98 | 99 | done(); 100 | } 101 | ); 102 | }); 103 | it('should publish built event', function (done) { 104 | waitUntil( 105 | function () { 106 | return res.events.length >= 3; 107 | }, 108 | function () { 109 | var event = JSON.parse(res.events[2].substring(5)); 110 | 111 | assert.equal(event.action, 'built'); 112 | assert.equal(event.name, ''); 113 | assert.ok(event.hash); 114 | assert.ok(event.time); 115 | assert.ok(Array.isArray(event.warnings)); 116 | assert.ok(Array.isArray(event.errors)); 117 | assert.ok(typeof event.modules === 'object'); 118 | 119 | done(); 120 | } 121 | ); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('realistic multi compiler', function () { 127 | var multiCompiler; 128 | 129 | var compilationConfig = [ 130 | { 131 | name: 'first', 132 | entryPath: path.join( 133 | __dirname, 134 | './fixtures/multi-compiler/first-compilation-client.js' 135 | ), 136 | }, 137 | { 138 | name: 'second', 139 | entryPath: path.join( 140 | __dirname, 141 | './fixtures/multi-compiler/second-compilation-client.js' 142 | ), 143 | }, 144 | ]; 145 | 146 | before(function () { 147 | multiCompiler = webpack( 148 | compilationConfig.map(function (compilation) { 149 | require('fs').writeFileSync( 150 | compilation.entryPath, 151 | 'var a = ' + Math.random() + ';\n' 152 | ); 153 | 154 | return { 155 | name: compilation.name, 156 | mode: 'development', 157 | entry: [ 158 | require.resolve(compilation.entryPath), 159 | path.join( 160 | __dirname, 161 | '../client.js' + 162 | '?=' + 163 | qs.stringify({ 164 | name: compilation.name, 165 | }) 166 | ), 167 | ], 168 | plugins: [new webpack.HotModuleReplacementPlugin()], 169 | }; 170 | }) 171 | ); 172 | 173 | app = express(); 174 | app.use( 175 | webpackDevMiddleware(multiCompiler, { 176 | publicPath: '/', 177 | // stats: 'none', 178 | }) 179 | ); 180 | app.use( 181 | webpackHotMiddleware(multiCompiler, { 182 | log: function () {}, 183 | }) 184 | ); 185 | }); 186 | 187 | it('should create eventStream on /__webpack_hmr', function (done) { 188 | request('/__webpack_hmr') 189 | .expect('Content-Type', /^text\/event-stream\b/) 190 | .end(done); 191 | }); 192 | 193 | describe('first build', function () { 194 | it('should publish sync event for all compilations from multi compiler', function (done) { 195 | request('/__webpack_hmr') 196 | .expect('Content-Type', /^text\/event-stream\b/) 197 | .end(function (err, res) { 198 | if (err) return done(err); 199 | 200 | assert.equal(res.events.length, compilationConfig.length); 201 | 202 | res.events.forEach(function (resEvent, idx) { 203 | var event = JSON.parse(resEvent.substring(5)); 204 | assert.equal(event.action, 'sync'); 205 | assert.equal(event.name, compilationConfig[idx].name); 206 | assert.ok(event.hash); 207 | assert.ok(event.time); 208 | assert.ok(Array.isArray(event.warnings)); 209 | assert.ok(Array.isArray(event.errors)); 210 | assert.ok(typeof event.modules === 'object'); 211 | }); 212 | 213 | done(); 214 | }); 215 | }); 216 | }); 217 | 218 | describe('after file change', function () { 219 | var res; 220 | 221 | before(function (done) { 222 | request('/__webpack_hmr') 223 | .expect('Content-Type', /^text\/event-stream\b/) 224 | .end(function (err, _res) { 225 | if (err) return done(err); 226 | 227 | res = _res; 228 | 229 | // simulate write to a random entry of the compilation config list 230 | require('fs').writeFile( 231 | compilationConfig[ 232 | Math.floor(Math.random() * compilationConfig.length) 233 | ].entryPath, 234 | 'var a = ' + Math.random() + ';\n', 235 | done 236 | ); 237 | }); 238 | }); 239 | 240 | it('should publish building event', function (done) { 241 | waitUntil( 242 | function () { 243 | // building phase is after sync phase, but because we only change one file 244 | // we expect compilationConfig length + 1 events 245 | return res.events.length >= compilationConfig.length + 1; 246 | }, 247 | function () { 248 | var phaseEvents = res.events.slice(compilationConfig.length); 249 | 250 | phaseEvents.forEach(function (phaseEvent) { 251 | var event = JSON.parse(phaseEvent.substring(5)); 252 | 253 | assert.equal(event.action, 'building'); 254 | }); 255 | 256 | done(); 257 | } 258 | ); 259 | }); 260 | 261 | it('should publish built event', function (done) { 262 | waitUntil( 263 | function () { 264 | // built is 3rd phase, right after building 265 | // we expect to have received 2 batches of events for all entries of the compilationConfig 266 | // in addition to the single event fired for our simulated write 267 | return res.events.length >= 2 * compilationConfig.length + 1; 268 | }, 269 | function () { 270 | var phaseEvents = res.events.slice(compilationConfig.length + 1); 271 | 272 | phaseEvents.forEach(function (phaseEvent, idx) { 273 | var event = JSON.parse(phaseEvent.substring(5)); 274 | 275 | assert.equal(event.action, 'built'); 276 | assert.equal(event.name, compilationConfig[idx].name); 277 | assert.ok(event.hash); 278 | assert.ok(event.time); 279 | assert.ok(Array.isArray(event.warnings)); 280 | assert.ok(Array.isArray(event.errors)); 281 | assert.ok(typeof event.modules === 'object'); 282 | }); 283 | 284 | done(); 285 | } 286 | ); 287 | }); 288 | }); 289 | }); 290 | 291 | function request(path) { 292 | // Wrap some stuff up so supertest works with streaming responses 293 | var req = supertest(app).get(path).buffer(false); 294 | var end = req.end; 295 | req.end = function (callback) { 296 | req.on('error', callback).on('response', function (res) { 297 | Object.defineProperty(res, 'events', { 298 | get: function () { 299 | return res.text.trim().split('\n\n'); 300 | }, 301 | }); 302 | res.on('data', function (chunk) { 303 | res.text = (res.text || '') + chunk; 304 | }); 305 | process.nextTick(function () { 306 | req.assert(null, res, function (err) { 307 | callback(err, res); 308 | }); 309 | }); 310 | }); 311 | 312 | end.call(req, function () {}); 313 | }; 314 | return req; 315 | } 316 | 317 | function waitUntil(condition, body) { 318 | if (condition()) { 319 | body(); 320 | } else { 321 | setTimeout(function () { 322 | waitUntil(condition, body); 323 | }, 50); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /test/middleware-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var events = require('events'); 3 | var assert = require('assert'); 4 | 5 | var sinon = require('sinon'); 6 | var supertest = require('supertest'); 7 | 8 | var express = require('express'); 9 | var webpackHotMiddleware = require('../middleware'); 10 | 11 | describe('middleware', function () { 12 | var s, compiler, app, middleware; 13 | 14 | context('with default options', function () { 15 | beforeEach(setupServer({ log: function () {} })); 16 | 17 | it('should create eventStream on /__webpack_hmr', function (done) { 18 | request('/__webpack_hmr') 19 | .expect('Content-Type', /^text\/event-stream\b/) 20 | .end(done); 21 | }); 22 | it('should heartbeat every 10 seconds', function (done) { 23 | request('/__webpack_hmr').end(function (err, res) { 24 | if (err) return done(err); 25 | 26 | // Tick 3 times, then verify 27 | var i = 0; 28 | tick(10, 'seconds'); 29 | res.on('data', function () { 30 | if (++i < 3) { 31 | tick(10, 'seconds'); 32 | } else { 33 | verify(); 34 | } 35 | }); 36 | 37 | function verify() { 38 | assert.equal(res.events.length, 3); 39 | res.events.every(function (chunk) { 40 | assert(/^data: /.test(chunk)); 41 | }); 42 | done(); 43 | } 44 | }); 45 | }); 46 | it('should notify clients when bundle rebuild begins', function (done) { 47 | request('/__webpack_hmr').end(function (err, res) { 48 | if (err) return done(err); 49 | 50 | res.on('data', verify); 51 | 52 | compiler.emit('invalid'); 53 | 54 | function verify() { 55 | assert.equal(res.events.length, 1); 56 | var event = JSON.parse(res.events[0].substring(5)); 57 | assert.equal(event.action, 'building'); 58 | done(); 59 | } 60 | }); 61 | }); 62 | it('should notify clients when bundle is complete', function (done) { 63 | request('/__webpack_hmr').end(function (err, res) { 64 | if (err) return done(err); 65 | 66 | res.on('data', verify); 67 | 68 | compiler.emit( 69 | 'done', 70 | stats({ 71 | time: 100, 72 | hash: 'deadbeeffeddad', 73 | warnings: false, 74 | errors: false, 75 | modules: [], 76 | }) 77 | ); 78 | 79 | function verify() { 80 | assert.equal(res.events.length, 1); 81 | var event = JSON.parse(res.events[0].substring(5)); 82 | assert.equal(event.action, 'built'); 83 | done(); 84 | } 85 | }); 86 | }); 87 | it('should notify clients when bundle is complete (multicompiler)', function (done) { 88 | request('/__webpack_hmr').end(function (err, res) { 89 | if (err) return done(err); 90 | 91 | res.once('data', verify); 92 | 93 | compiler.emit( 94 | 'done', 95 | stats({ 96 | children: [ 97 | { 98 | time: 100, 99 | hash: 'deadbeeffeddad', 100 | warnings: false, 101 | errors: false, 102 | modules: [], 103 | }, 104 | { 105 | time: 150, 106 | hash: 'gwegawefawefawef', 107 | warnings: false, 108 | errors: false, 109 | modules: [], 110 | }, 111 | ], 112 | }) 113 | ); 114 | 115 | function verify() { 116 | assert.equal(res.events.length, 1); 117 | var event = JSON.parse(res.events[0].substring(5)); 118 | assert.equal(event.action, 'built'); 119 | done(); 120 | } 121 | }); 122 | }); 123 | it('should notify clients when bundle is complete (webpack5 multicompiler)', function (done) { 124 | request('/__webpack_hmr').end(function (err, res) { 125 | if (err) return done(err); 126 | 127 | res.once('data', verify); 128 | 129 | var multiStats = Array.from(new Array(2)).map(function (_, idx) { 130 | var hash = 131 | 'deadbeeffeddad' + 132 | String.prototype.toLowerCase.call( 133 | String.fromCharCode((idx % 26) + 65) 134 | ); 135 | return Object.assign( 136 | stats({ 137 | time: 100, 138 | hash: hash, 139 | warnings: false, 140 | errors: false, 141 | modules: [], 142 | }), 143 | { compilation: { name: idx } } 144 | ); 145 | }); 146 | 147 | compiler.emit('done', { 148 | toJson: function () { 149 | //not implemented 150 | }, 151 | stats: multiStats, 152 | }); 153 | 154 | function verify() { 155 | assert.equal(res.events.length, 1); 156 | var event = JSON.parse(res.events[0].substring(5)); 157 | assert.equal(event.action, 'built'); 158 | assert.equal(event.hash, multiStats[0].toJson().hash); 159 | done(); 160 | } 161 | }); 162 | }); 163 | it('should notify new clients about current compilation state', function (done) { 164 | compiler.emit( 165 | 'done', 166 | stats({ 167 | time: 100, 168 | hash: 'deadbeeffeddad', 169 | warnings: false, 170 | errors: false, 171 | modules: [], 172 | }) 173 | ); 174 | 175 | request('/__webpack_hmr').end(function (err, res) { 176 | if (err) return done(err); 177 | assert.equal(res.events.length, 1); 178 | var event = JSON.parse(res.events[0].substring(5)); 179 | assert.equal(event.action, 'sync'); 180 | done(); 181 | }); 182 | }); 183 | it('should fallback to the compilation name if no stats name is provided and there is one stats object', function (done) { 184 | compiler.emit( 185 | 'done', 186 | stats({ 187 | time: 100, 188 | hash: 'deadbeeffeddad', 189 | warnings: false, 190 | errors: false, 191 | modules: [], 192 | }) 193 | ); 194 | 195 | request('/__webpack_hmr').end(function (err, res) { 196 | if (err) return done(err); 197 | 198 | var event = JSON.parse(res.events[0].substring(5)); 199 | assert.equal(event.name, 'compilation'); 200 | done(); 201 | }); 202 | }); 203 | it('should have tests on the payload of bundle complete'); 204 | it('should notify all clients', function (done) { 205 | request('/__webpack_hmr').end(function (err, res) { 206 | if (err) return done(err); 207 | res.on('data', verify); 208 | when(); 209 | }); 210 | request('/__webpack_hmr').end(function (err, res) { 211 | if (err) return done(err); 212 | res.on('data', verify); 213 | when(); 214 | }); 215 | 216 | // Emit compile when both requests are connected 217 | when.n = 0; 218 | function when() { 219 | if (++when.n < 2) return; 220 | 221 | compiler.emit('invalid'); 222 | } 223 | 224 | // Finish test when both requests report data 225 | verify.n = 0; 226 | function verify() { 227 | if (++verify.n < 2) return; 228 | 229 | done(); 230 | } 231 | }); 232 | it('should allow custom events to be published', function (done) { 233 | request('/__webpack_hmr').end(function (err, res) { 234 | if (err) return done(err); 235 | res.on('data', verify); 236 | 237 | middleware.publish({ obj: 'with stuff' }); 238 | 239 | function verify() { 240 | assert.equal(res.events.length, 1); 241 | var event = JSON.parse(res.events[0].substring(5)); 242 | assert.deepEqual(event, { obj: 'with stuff' }); 243 | done(); 244 | } 245 | }); 246 | }); 247 | // Express HTTP/2 support is in progress: https://github.com/expressjs/express/pull/3390 248 | it('should not contain `connection: keep-alive` header for HTTP/2 request'); 249 | it('should contain `connection: keep-alive` header for HTTP/1 request', function (done) { 250 | request('/__webpack_hmr').end(function (err, res) { 251 | if (err) return done(err); 252 | assert.equal(res.headers['connection'], 'keep-alive'); 253 | done(); 254 | }); 255 | }); 256 | 257 | it('should end event stream clients and disable compiler hooks on close', function (done) { 258 | request('/__webpack_hmr').end(function (err, res) { 259 | if (err) return done(err); 260 | 261 | var called = 0; 262 | res.on('data', function () { 263 | called++; 264 | }); 265 | 266 | res.on('end', function () { 267 | middleware({}, {}, function (err) { 268 | assert(!err); 269 | assert.equal(called, 3); 270 | done(); 271 | }); 272 | }); 273 | 274 | middleware.publish({ obj: 'with stuff' }); 275 | compiler.emit('invalid'); 276 | compiler.emit('done', stats({ modules: [] })); 277 | middleware.close(); 278 | middleware.publish({ obj: 'with stuff' }); 279 | compiler.emit('invalid'); 280 | compiler.emit('done', stats({ modules: [] })); 281 | }); 282 | }); 283 | }); 284 | 285 | beforeEach(function () { 286 | s = sinon.createSandbox(); 287 | s.useFakeTimers(); 288 | compiler = new events.EventEmitter(); 289 | compiler.plugin = compiler.on; 290 | }); 291 | afterEach(function () { 292 | s.restore(); 293 | }); 294 | function tick(time, unit) { 295 | if (unit == 'seconds') time *= 1000; 296 | s.clock.tick(time + 10); // +10ms for some leeway 297 | } 298 | function setupServer(opts) { 299 | return function () { 300 | app = express(); 301 | middleware = webpackHotMiddleware(compiler, opts); 302 | app.use(middleware); 303 | }; 304 | } 305 | function request(path) { 306 | // Wrap some stuff up so supertest works with streaming responses 307 | var req = supertest(app).get(path).buffer(false); 308 | var end = req.end; 309 | req.end = function (callback) { 310 | req.on('error', callback).on('response', function (res) { 311 | Object.defineProperty(res, 'events', { 312 | get: function () { 313 | return res.text.trim().split('\n\n'); 314 | }, 315 | }); 316 | res.on('data', function (chunk) { 317 | res.text = (res.text || '') + chunk; 318 | }); 319 | process.nextTick(function () { 320 | req.assert(null, res, function (err) { 321 | callback(err, res); 322 | }); 323 | }); 324 | }); 325 | 326 | end.call(req, function () {}); 327 | }; 328 | return req; 329 | } 330 | function stats(data) { 331 | return { 332 | compilation: { 333 | name: 'compilation', 334 | }, 335 | toJson: function () { 336 | return data; 337 | }, 338 | }; 339 | } 340 | }); 341 | -------------------------------------------------------------------------------- /test/client-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, browser */ 2 | 3 | var sinon = require('sinon'); 4 | 5 | describe('client', function () { 6 | var s, client, clientOverlay, processUpdate; 7 | 8 | beforeEach(function () { 9 | s = sinon.createSandbox({ useFakeTimers: true }); 10 | }); 11 | afterEach(function () { 12 | s.restore(); 13 | }); 14 | 15 | context('with default options', function () { 16 | beforeEach(function setup() { 17 | global.__resourceQuery = ''; // eslint-disable-line no-underscore-dangle 18 | global.document = {}; 19 | global.window = { 20 | EventSource: sinon.stub().returns({ 21 | close: sinon.spy(), 22 | }), 23 | }; 24 | }); 25 | beforeEach(loadClient); 26 | it('should connect to __webpack_hmr', function () { 27 | sinon.assert.calledOnce(window.EventSource); 28 | sinon.assert.calledWithNew(window.EventSource); 29 | sinon.assert.calledWith(window.EventSource, '/__webpack_hmr'); 30 | }); 31 | it('should trigger webpack on successful builds', function () { 32 | var eventSource = window.EventSource.lastCall.returnValue; 33 | eventSource.onmessage( 34 | makeMessage({ 35 | action: 'built', 36 | time: 100, 37 | hash: 'deadbeeffeddad', 38 | errors: [], 39 | warnings: [], 40 | modules: [], 41 | }) 42 | ); 43 | sinon.assert.calledOnce(processUpdate); 44 | }); 45 | it('should trigger webpack on successful syncs', function () { 46 | var eventSource = window.EventSource.lastCall.returnValue; 47 | eventSource.onmessage( 48 | makeMessage({ 49 | action: 'sync', 50 | time: 100, 51 | hash: 'deadbeeffeddad', 52 | errors: [], 53 | warnings: [], 54 | modules: [], 55 | }) 56 | ); 57 | sinon.assert.calledOnce(processUpdate); 58 | }); 59 | it('should call subscribeAll handler on default messages', function () { 60 | var spy = sinon.spy(); 61 | client.subscribeAll(spy); 62 | var message = { 63 | action: 'built', 64 | time: 100, 65 | hash: 'deadbeeffeddad', 66 | errors: [], 67 | warnings: [], 68 | modules: [], 69 | }; 70 | 71 | var eventSource = window.EventSource.lastCall.returnValue; 72 | eventSource.onmessage(makeMessage(message)); 73 | 74 | sinon.assert.calledOnce(spy); 75 | sinon.assert.calledWith(spy, message); 76 | }); 77 | it('should call subscribeAll handler on custom messages', function () { 78 | var spy = sinon.spy(); 79 | client.subscribeAll(spy); 80 | 81 | var eventSource = window.EventSource.lastCall.returnValue; 82 | eventSource.onmessage( 83 | makeMessage({ 84 | action: 'thingy', 85 | }) 86 | ); 87 | 88 | sinon.assert.calledOnce(spy); 89 | sinon.assert.calledWith(spy, { action: 'thingy' }); 90 | }); 91 | it('should call only custom handler on custom messages', function () { 92 | var spy = sinon.spy(); 93 | client.subscribe(spy); 94 | 95 | var eventSource = window.EventSource.lastCall.returnValue; 96 | eventSource.onmessage( 97 | makeMessage({ 98 | custom: 'thingy', 99 | }) 100 | ); 101 | eventSource.onmessage( 102 | makeMessage({ 103 | action: 'built', 104 | }) 105 | ); 106 | 107 | sinon.assert.calledOnce(spy); 108 | sinon.assert.calledWith(spy, { custom: 'thingy' }); 109 | sinon.assert.notCalled(processUpdate); 110 | }); 111 | it('should not trigger webpack on errored builds', function () { 112 | var eventSource = window.EventSource.lastCall.returnValue; 113 | eventSource.onmessage( 114 | makeMessage({ 115 | action: 'built', 116 | time: 100, 117 | hash: 'deadbeeffeddad', 118 | errors: ['Something broke'], 119 | warnings: [], 120 | modules: [], 121 | }) 122 | ); 123 | sinon.assert.notCalled(processUpdate); 124 | }); 125 | it('should show overlay on errored builds', function () { 126 | var eventSource = window.EventSource.lastCall.returnValue; 127 | eventSource.onmessage( 128 | makeMessage({ 129 | action: 'built', 130 | time: 100, 131 | hash: 'deadbeeffeddad', 132 | errors: ['Something broke', 'Actually, 2 things broke'], 133 | warnings: [], 134 | modules: [], 135 | }) 136 | ); 137 | sinon.assert.calledOnce(clientOverlay.showProblems); 138 | sinon.assert.calledWith(clientOverlay.showProblems, 'errors', [ 139 | 'Something broke', 140 | 'Actually, 2 things broke', 141 | ]); 142 | }); 143 | it('should hide overlay after errored build fixed', function () { 144 | var eventSource = window.EventSource.lastCall.returnValue; 145 | eventSource.onmessage( 146 | makeMessage({ 147 | action: 'built', 148 | time: 100, 149 | hash: 'deadbeeffeddad', 150 | errors: ['Something broke', 'Actually, 2 things broke'], 151 | warnings: [], 152 | modules: [], 153 | }) 154 | ); 155 | eventSource.onmessage( 156 | makeMessage({ 157 | action: 'built', 158 | time: 100, 159 | hash: 'deadbeeffeddad', 160 | errors: [], 161 | warnings: [], 162 | modules: [], 163 | }) 164 | ); 165 | sinon.assert.calledOnce(clientOverlay.showProblems); 166 | sinon.assert.calledOnce(clientOverlay.clear); 167 | }); 168 | it('should hide overlay after errored build becomes warning', function () { 169 | var eventSource = window.EventSource.lastCall.returnValue; 170 | eventSource.onmessage( 171 | makeMessage({ 172 | action: 'built', 173 | time: 100, 174 | hash: 'deadbeeffeddad', 175 | errors: ['Something broke', 'Actually, 2 things broke'], 176 | warnings: [], 177 | modules: [], 178 | }) 179 | ); 180 | eventSource.onmessage( 181 | makeMessage({ 182 | action: 'built', 183 | time: 100, 184 | hash: 'deadbeeffeddad', 185 | errors: [], 186 | warnings: ["This isn't great, but it's not terrible"], 187 | modules: [], 188 | }) 189 | ); 190 | sinon.assert.calledOnce(clientOverlay.showProblems); 191 | sinon.assert.calledOnce(clientOverlay.clear); 192 | }); 193 | it('should trigger webpack on warning builds', function () { 194 | var eventSource = window.EventSource.lastCall.returnValue; 195 | eventSource.onmessage( 196 | makeMessage({ 197 | action: 'built', 198 | time: 100, 199 | hash: 'deadbeeffeddad', 200 | errors: [], 201 | warnings: ["This isn't great, but it's not terrible"], 202 | modules: [], 203 | }) 204 | ); 205 | sinon.assert.calledOnce(processUpdate); 206 | }); 207 | it('should not overlay on warning builds', function () { 208 | var eventSource = window.EventSource.lastCall.returnValue; 209 | eventSource.onmessage( 210 | makeMessage({ 211 | action: 'built', 212 | time: 100, 213 | hash: 'deadbeeffeddad', 214 | errors: [], 215 | warnings: ["This isn't great, but it's not terrible"], 216 | modules: [], 217 | }) 218 | ); 219 | sinon.assert.notCalled(clientOverlay.showProblems); 220 | }); 221 | it('should show overlay after warning build becomes error', function () { 222 | var eventSource = window.EventSource.lastCall.returnValue; 223 | eventSource.onmessage( 224 | makeMessage({ 225 | action: 'built', 226 | time: 100, 227 | hash: 'deadbeeffeddad', 228 | errors: [], 229 | warnings: ["This isn't great, but it's not terrible"], 230 | modules: [], 231 | }) 232 | ); 233 | eventSource.onmessage( 234 | makeMessage({ 235 | action: 'built', 236 | time: 100, 237 | hash: 'deadbeeffeddad', 238 | errors: ['Something broke', 'Actually, 2 things broke'], 239 | warnings: [], 240 | modules: [], 241 | }) 242 | ); 243 | sinon.assert.calledOnce(clientOverlay.showProblems); 244 | }); 245 | it("should test more of the client's functionality"); 246 | }); 247 | 248 | context('with overlayWarnings: true', function () { 249 | beforeEach(function setup() { 250 | global.__resourceQuery = '?overlayWarnings=true'; // eslint-disable-line no-underscore-dangle 251 | global.document = {}; 252 | global.window = { 253 | EventSource: sinon.stub().returns({ 254 | close: sinon.spy(), 255 | }), 256 | }; 257 | }); 258 | beforeEach(loadClient); 259 | it('should show overlay on errored builds', function () { 260 | var eventSource = window.EventSource.lastCall.returnValue; 261 | eventSource.onmessage( 262 | makeMessage({ 263 | action: 'built', 264 | time: 100, 265 | hash: 'deadbeeffeddad', 266 | errors: ['Something broke', 'Actually, 2 things broke'], 267 | warnings: [], 268 | modules: [], 269 | }) 270 | ); 271 | sinon.assert.calledOnce(clientOverlay.showProblems); 272 | sinon.assert.calledWith(clientOverlay.showProblems, 'errors', [ 273 | 'Something broke', 274 | 'Actually, 2 things broke', 275 | ]); 276 | }); 277 | it('should hide overlay after errored build fixed', function () { 278 | var eventSource = window.EventSource.lastCall.returnValue; 279 | eventSource.onmessage( 280 | makeMessage({ 281 | action: 'built', 282 | time: 100, 283 | hash: 'deadbeeffeddad', 284 | errors: ['Something broke', 'Actually, 2 things broke'], 285 | warnings: [], 286 | modules: [], 287 | }) 288 | ); 289 | eventSource.onmessage( 290 | makeMessage({ 291 | action: 'built', 292 | time: 100, 293 | hash: 'deadbeeffeddad', 294 | errors: [], 295 | warnings: [], 296 | modules: [], 297 | }) 298 | ); 299 | sinon.assert.calledOnce(clientOverlay.showProblems); 300 | sinon.assert.calledOnce(clientOverlay.clear); 301 | }); 302 | it('should show overlay on warning builds', function () { 303 | var eventSource = window.EventSource.lastCall.returnValue; 304 | eventSource.onmessage( 305 | makeMessage({ 306 | action: 'built', 307 | time: 100, 308 | hash: 'deadbeeffeddad', 309 | errors: [], 310 | warnings: ["This isn't great, but it's not terrible"], 311 | modules: [], 312 | }) 313 | ); 314 | sinon.assert.calledOnce(clientOverlay.showProblems); 315 | sinon.assert.calledWith(clientOverlay.showProblems, 'warnings', [ 316 | "This isn't great, but it's not terrible", 317 | ]); 318 | }); 319 | it('should hide overlay after warning build fixed', function () { 320 | var eventSource = window.EventSource.lastCall.returnValue; 321 | eventSource.onmessage( 322 | makeMessage({ 323 | action: 'built', 324 | time: 100, 325 | hash: 'deadbeeffeddad', 326 | errors: [], 327 | warnings: ["This isn't great, but it's not terrible"], 328 | modules: [], 329 | }) 330 | ); 331 | eventSource.onmessage( 332 | makeMessage({ 333 | action: 'built', 334 | time: 100, 335 | hash: 'deadbeeffeddad', 336 | errors: [], 337 | warnings: [], 338 | modules: [], 339 | }) 340 | ); 341 | sinon.assert.calledOnce(clientOverlay.showProblems); 342 | sinon.assert.calledOnce(clientOverlay.clear); 343 | }); 344 | it('should update overlay after errored build becomes warning', function () { 345 | var eventSource = window.EventSource.lastCall.returnValue; 346 | eventSource.onmessage( 347 | makeMessage({ 348 | action: 'built', 349 | time: 100, 350 | hash: 'deadbeeffeddad', 351 | errors: ['Something broke', 'Actually, 2 things broke'], 352 | warnings: [], 353 | modules: [], 354 | }) 355 | ); 356 | eventSource.onmessage( 357 | makeMessage({ 358 | action: 'built', 359 | time: 100, 360 | hash: 'deadbeeffeddad', 361 | errors: [], 362 | warnings: ["This isn't great, but it's not terrible"], 363 | modules: [], 364 | }) 365 | ); 366 | sinon.assert.calledTwice(clientOverlay.showProblems); 367 | sinon.assert.calledWith(clientOverlay.showProblems, 'errors'); 368 | sinon.assert.calledWith(clientOverlay.showProblems, 'warnings'); 369 | }); 370 | }); 371 | 372 | context('with name options', function () { 373 | beforeEach(function setup() { 374 | global.__resourceQuery = '?name=test'; // eslint-disable-line no-underscore-dangle 375 | global.window = { 376 | EventSource: sinon.stub().returns({ 377 | close: sinon.spy(), 378 | }), 379 | }; 380 | }); 381 | beforeEach(loadClient); 382 | it('should not trigger webpack if event obj name is different', function () { 383 | var eventSource = window.EventSource.lastCall.returnValue; 384 | eventSource.onmessage( 385 | makeMessage({ 386 | name: 'foo', 387 | action: 'built', 388 | time: 100, 389 | hash: 'deadbeeffeddad', 390 | errors: [], 391 | warnings: [], 392 | modules: [], 393 | }) 394 | ); 395 | sinon.assert.notCalled(processUpdate); 396 | }); 397 | it('should not trigger webpack on successful syncs if obj name is different', function () { 398 | var eventSource = window.EventSource.lastCall.returnValue; 399 | eventSource.onmessage( 400 | makeMessage({ 401 | name: 'bar', 402 | action: 'sync', 403 | time: 100, 404 | hash: 'deadbeeffeddad', 405 | errors: [], 406 | warnings: [], 407 | modules: [], 408 | }) 409 | ); 410 | sinon.assert.notCalled(processUpdate); 411 | }); 412 | }); 413 | 414 | context('with no browser environment', function () { 415 | beforeEach(function setup() { 416 | global.__resourceQuery = ''; // eslint-disable-line no-underscore-dangle 417 | delete global.window; 418 | }); 419 | beforeEach(loadClient); 420 | it('should not connect', function () { 421 | // doesn't error 422 | }); 423 | }); 424 | 425 | context('with no EventSource', function () { 426 | beforeEach(function setup() { 427 | global.__resourceQuery = ''; // eslint-disable-line no-underscore-dangle 428 | global.window = {}; 429 | s.stub(console, 'warn'); 430 | }); 431 | beforeEach(loadClient); 432 | it('should emit warning and not connect', function () { 433 | sinon.assert.calledOnce(console.warn); 434 | sinon.assert.calledWithMatch(console.warn, /EventSource/); 435 | }); 436 | }); 437 | 438 | function makeMessage(obj) { 439 | return { data: typeof obj === 'string' ? obj : JSON.stringify(obj) }; 440 | } 441 | 442 | function loadClient() { 443 | var path = require.resolve('../client'); 444 | delete require.cache[path]; 445 | client = require(path); 446 | } 447 | 448 | beforeEach(function () { 449 | clientOverlay = { showProblems: sinon.stub(), clear: sinon.stub() }; 450 | var clientOverlayModule = { 451 | exports: function () { 452 | return clientOverlay; 453 | }, 454 | }; 455 | require.cache[require.resolve('../client-overlay')] = clientOverlayModule; 456 | 457 | processUpdate = sinon.stub(); 458 | require.cache[require.resolve('../process-update')] = { 459 | exports: processUpdate, 460 | }; 461 | }); 462 | afterEach(function () { 463 | delete require.cache[require.resolve('../client-overlay')]; 464 | delete require.cache[require.resolve('../process-update')]; 465 | }); 466 | }); 467 | --------------------------------------------------------------------------------