diff --git a/bin/options.js b/bin/options.js index f4c7f8919e..41dffeb8f4 100644 --- a/bin/options.js +++ b/bin/options.js @@ -17,6 +17,11 @@ const options = { type: 'boolean', describe: 'Broadcasts the server via ZeroConf networking on start', }, + liveReload: { + type: 'boolean', + describe: 'Enables/Disables live reloading on changing files', + default: true, + }, lazy: { type: 'boolean', describe: 'Lazy', diff --git a/client-src/default/index.js b/client-src/default/index.js index 0ea9f592a6..489c762c9b 100644 --- a/client-src/default/index.js +++ b/client-src/default/index.js @@ -47,6 +47,7 @@ if (!urlParts.port || urlParts.port === '0') { } let hot = false; +let liveReload = false; let initial = true; let currentHash = ''; let useWarningOverlay = false; @@ -85,6 +86,10 @@ const onSocketMsg = { hot = true; log.info('[WDS] Hot Module Replacement enabled.'); }, + liveReload() { + liveReload = true; + log.info('[WDS] Live Reloading enabled.'); + }, invalid() { log.info('[WDS] App updated. Recompiling...'); // fixes #1042. overlay doesn't clear if errors are fixed but warnings remain. @@ -271,7 +276,9 @@ function reloadApp() { // broadcast update to window self.postMessage(`webpackHotUpdate${currentHash}`, '*'); } - } else { + } + // allow refreshing the page only if liveReload isn't disabled + if (liveReload) { let rootWindow = self; // use parent window for reload (in case we're in an iframe with no valid src) const intervalId = self.setInterval(() => { diff --git a/lib/Server.js b/lib/Server.js index ac335641bf..d4e9b1a484 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -869,6 +869,10 @@ class Server { this.sockWrite([connection], 'progress', this.progress); } + if (this.options.liveReload !== false) { + this.sockWrite([connection], 'liveReload', this.options.liveReload); + } + if (this.clientOverlay) { this.sockWrite([connection], 'overlay', this.clientOverlay); } @@ -997,9 +1001,12 @@ class Server { const watcher = chokidar.watch(watchPath, watchOptions); - watcher.on('change', () => { - this.sockWrite(this.sockets, 'content-changed'); - }); + // disabling refreshing on changing the content + if (this.options.liveReload !== false) { + watcher.on('change', () => { + this.sockWrite(this.sockets, 'content-changed'); + }); + } this.contentBaseWatchers.push(watcher); } diff --git a/lib/options.json b/lib/options.json index 2f5b32caa5..c1b7759e2b 100644 --- a/lib/options.json +++ b/lib/options.json @@ -168,6 +168,9 @@ "lazy": { "type": "boolean" }, + "liveReload": { + "type": "boolean" + }, "log": { "instanceof": "Function" }, @@ -377,6 +380,7 @@ "inline": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverinline)", "key": "should be {String|Buffer}", "lazy": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverlazy-)", + "liveReload": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#livereload-)", "log": "should be {Function}", "logLevel": "should be {String} and equal to one of the allowed values\n\n [ 'info', 'warn', 'error', 'debug', 'trace', 'silent' ]\n\n (https://github.com/webpack/webpack-dev-middleware#loglevel)", "logTime": "should be {Boolean} (https://github.com/webpack/webpack-dev-middleware#logtime)", diff --git a/lib/utils/createConfig.js b/lib/utils/createConfig.js index 510b3f7307..00cd8536fe 100644 --- a/lib/utils/createConfig.js +++ b/lib/utils/createConfig.js @@ -88,6 +88,10 @@ function createConfig(config, argv, { port }) { options.hotOnly = argv.hotOnly; } + if (argv.liveReload === false) { + options.liveReload = false; + } + // TODO https://github.com/webpack/webpack-dev-server/issues/616 (v4) // We should prefer CLI arg under config, now we always prefer `clientLogLevel` from `devServer` if (!options.clientLogLevel && argv.clientLogLevel) { diff --git a/test/ContentBase.test.js b/test/ContentBase.test.js index bf743aac7c..d567cc57cd 100644 --- a/test/ContentBase.test.js +++ b/test/ContentBase.test.js @@ -19,6 +19,110 @@ describe('ContentBase', () => { let server; let req; + describe('Test disabling live reloading', () => { + const nestedFile = path.join(contentBasePublic, 'assets/example.txt'); + + jest.setTimeout(30000); + + beforeAll((done) => { + server = helper.start( + config, + { + contentBase: contentBasePublic, + watchContentBase: true, + liveReload: false, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + helper.close(() => { + done(); + }); + fs.truncateSync(nestedFile); + }); + + it('Should not reload on changing files', (done) => { + let reloaded = false; + + server.contentBaseWatchers[0].on('change', () => { + // it means that file has changed + + // simulating server behaviour + if (server.options.liveReload !== false) { + Object.defineProperty(window.location, 'reload', { + configurable: true, + }); + window.location.reload = jest.fn(); + window.location.reload(); + reloaded = true; + } + expect(reloaded).toBe(false); + + done(); + }); + + // change file content + setTimeout(() => { + fs.writeFileSync(nestedFile, 'Heyo', 'utf8'); + }, 1000); + }); + }); + + describe('Testing live reloading', () => { + const nestedFile = path.join(contentBasePublic, 'assets/example.txt'); + + jest.setTimeout(30000); + + beforeAll((done) => { + server = helper.start( + config, + { + contentBase: contentBasePublic, + watchContentBase: true, + liveReload: true, + }, + done + ); + req = request(server.app); + }); + + afterAll((done) => { + helper.close(() => { + done(); + }); + fs.truncateSync(nestedFile); + }); + + it('Should reload on changing files', (done) => { + let reloaded = false; + + server.contentBaseWatchers[0].on('change', () => { + // it means that files has changed + + // simulating server behaviour + if (server.options.liveReload !== false) { + Object.defineProperty(window.location, 'reload', { + configurable: true, + }); + window.location.reload = jest.fn(); + window.location.reload(); + reloaded = true; + } + expect(reloaded).toBe(true); + + done(); + }); + + // change file content + setTimeout(() => { + fs.writeFileSync(nestedFile, 'Heyo', 'utf8'); + }, 1000); + }); + }); + describe('to directory', () => { const nestedFile = path.join(contentBasePublic, 'assets/example.txt'); diff --git a/test/CreateConfig.test.js b/test/CreateConfig.test.js index 97c5d47cba..6ea25efa12 100644 --- a/test/CreateConfig.test.js +++ b/test/CreateConfig.test.js @@ -11,6 +11,8 @@ const argv = { hot: true, // Can be `--no-hot-only` in CLI (misleading and undocumented) hotOnly: false, + // Can be `--live-reload` in CLI (misleading and undocumented) + liveReload: true, }; describe('createConfig', () => { @@ -346,6 +348,28 @@ describe('createConfig', () => { expect(config).toMatchSnapshot(); }); + it('liveReload option', () => { + const config = createConfig( + webpackConfig, + Object.assign({}, argv, { liveReload: true }), + { port: 8080 } + ); + + expect(config).toMatchSnapshot(); + }); + + it('liveReload option (in devServer config)', () => { + const config = createConfig( + Object.assign({}, webpackConfig, { + devServer: { liveReload: true }, + }), + argv, + { port: 8080 } + ); + + expect(config).toMatchSnapshot(); + }); + it('hot option (in devServer config)', () => { const config = createConfig( Object.assign({}, webpackConfig, { diff --git a/test/__snapshots__/CreateConfig.test.js.snap b/test/__snapshots__/CreateConfig.test.js.snap index a3ec18de9c..18faabada6 100644 --- a/test/__snapshots__/CreateConfig.test.js.snap +++ b/test/__snapshots__/CreateConfig.test.js.snap @@ -699,6 +699,35 @@ Object { } `; +exports[`createConfig liveReload option (in devServer config) 1`] = ` +Object { + "hot": true, + "hotOnly": false, + "liveReload": true, + "noInfo": true, + "port": 8080, + "publicPath": "/", + "stats": Object { + "cached": false, + "cachedAssets": false, + }, +} +`; + +exports[`createConfig liveReload option 1`] = ` +Object { + "hot": true, + "hotOnly": false, + "noInfo": true, + "port": 8080, + "publicPath": "/", + "stats": Object { + "cached": false, + "cachedAssets": false, + }, +} +`; + exports[`createConfig mimeTypes option - with force 1`] = ` Object { "hot": true,