├── .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 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/example/index-multientry.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Webpack Hot Middleware Multiple Entry Point Example
7 |
8 |
9 |
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 | [](https://www.npmjs.com/package/webpack-hot-middleware) [](https://circleci.com/gh/webpack/webpack-hot-middleware/tree/main)[](https://codecov.io/gh/webpack/webpack-hot-middleware)
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 |
--------------------------------------------------------------------------------