diff --git a/lib/Server.js b/lib/Server.js index 1c58eaea43..28986dfdaa 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -54,8 +54,6 @@ class Server { validateOptions(schema, options, 'webpack Dev Server'); - updateCompiler(compiler, options); - this.compiler = compiler; this.options = options; @@ -67,6 +65,7 @@ class Server { this.log = _log || createLogger(options); + // set serverMode default if (this.options.serverMode === undefined) { this.options.serverMode = 'sockjs'; } else { @@ -74,6 +73,16 @@ class Server { 'serverMode is an experimental option, meaning its usage could potentially change without warning' ); } + // set clientMode default + if (this.options.clientMode === undefined) { + this.options.clientMode = 'sockjs'; + } else { + this.log.warn( + 'clientMode is an experimental option, meaning its usage could potentially change without warning' + ); + } + + updateCompiler(this.compiler, this.options); // this.SocketServerImplementation is a class, so it must be instantiated before use this.socketServerImplementation = getSocketServerImplementation( diff --git a/lib/options.json b/lib/options.json index 349973498b..f410e4927b 100644 --- a/lib/options.json +++ b/lib/options.json @@ -48,6 +48,9 @@ "warning" ] }, + "clientMode": { + "type": "string" + }, "compress": { "type": "boolean" }, @@ -390,6 +393,7 @@ "ca": "should be {String|Buffer}", "cert": "should be {String|Buffer}", "clientLogLevel": "should be {String} and equal to one of the allowed values\n\n [ 'none', 'silent', 'info', 'debug', 'trace', 'error', 'warning', 'warn' ]\n\n (https://webpack.js.org/configuration/dev-server/#devserverclientloglevel)", + "clientMode": "should be {String} (https://webpack.js.org/configuration/dev-server/#devserverclientmode)", "compress": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devservercompress)", "contentBase": "should be {Number|String|Array} (https://webpack.js.org/configuration/dev-server/#devservercontentbase)", "disableHostCheck": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverdisablehostcheck)", @@ -430,7 +434,7 @@ "reporter": "should be {Function} (https://github.com/webpack/webpack-dev-middleware#reporter)", "requestCert": "should be {Boolean}", "serveIndex": "should be {Boolean} (https://webpack.js.org/configuration/dev-server/#devserverserveindex)", - "serverMode": "should be {String|Function} (https://webpack.js.org/configuration/dev-server/#devserverservermode-)", + "serverMode": "should be {String|Function} (https://webpack.js.org/configuration/dev-server/#devserverservermode)", "serverSideRender": "should be {Boolean} (https://github.com/webpack/webpack-dev-middleware#serversiderender)", "setup": "should be {Function} (https://webpack.js.org/configuration/dev-server/#devserversetup)", "sockHost": "should be {String|Null} (https://webpack.js.org/configuration/dev-server/#devserversockhost)", diff --git a/lib/utils/getSocketClientPath.js b/lib/utils/getSocketClientPath.js new file mode 100644 index 0000000000..950d07802a --- /dev/null +++ b/lib/utils/getSocketClientPath.js @@ -0,0 +1,36 @@ +'use strict'; + +function getSocketClientPath(options) { + let ClientImplementation; + let clientImplFound = true; + switch (typeof options.clientMode) { + case 'string': + // could be 'sockjs', in the future 'ws', or a path that should be required + if (options.clientMode === 'sockjs') { + // eslint-disable-next-line global-require + ClientImplementation = require('../../client/clients/SockJSClient'); + } else { + try { + // eslint-disable-next-line global-require, import/no-dynamic-require + ClientImplementation = require(options.clientMode); + } catch (e) { + clientImplFound = false; + } + } + break; + default: + clientImplFound = false; + } + + if (!clientImplFound) { + throw new Error( + "clientMode must be a string denoting a default implementation (e.g. 'sockjs') or a full path to " + + 'a JS file which exports a class extending BaseClient (webpack-dev-server/client-src/clients/BaseClient) ' + + 'via require.resolve(...)' + ); + } + + return ClientImplementation.getClientPath(options); +} + +module.exports = getSocketClientPath; diff --git a/lib/utils/updateCompiler.js b/lib/utils/updateCompiler.js index 60e7ff8a5a..9f701e81b4 100644 --- a/lib/utils/updateCompiler.js +++ b/lib/utils/updateCompiler.js @@ -6,6 +6,7 @@ */ const webpack = require('webpack'); const addEntries = require('./addEntries'); +const getSocketClientPath = require('./getSocketClientPath'); function updateCompiler(compiler, options) { if (options.inline !== false) { @@ -50,10 +51,7 @@ function updateCompiler(compiler, options) { compiler.hooks.entryOption.call(config.context, config.entry); const providePlugin = new webpack.ProvidePlugin({ - // SockJSClient.getClientPath(options) - __webpack_dev_server_client__: require.resolve( - '../../client/clients/SockJSClient.js' - ), + __webpack_dev_server_client__: getSocketClientPath(options), }); providePlugin.apply(compiler); }); diff --git a/test/e2e/ClientMode.test.js b/test/e2e/ClientMode.test.js new file mode 100644 index 0000000000..8f7b5d7a54 --- /dev/null +++ b/test/e2e/ClientMode.test.js @@ -0,0 +1,82 @@ +'use strict'; + +const testServer = require('../helpers/test-server'); +const config = require('../fixtures/client-config/webpack.config'); +const runBrowser = require('../helpers/run-browser'); +const port = require('../ports-map').ClientMode; + +describe('clientMode', () => { + describe('sockjs', () => { + beforeAll((done) => { + const options = { + port, + host: '0.0.0.0', + inline: true, + clientMode: 'sockjs', + }; + testServer.startAwaitingCompilation(config, options, done); + }); + + describe('on browser client', () => { + it('logs as usual', (done) => { + runBrowser().then(({ page, browser }) => { + const res = []; + page.goto(`http://localhost:${port}/main`); + page.on('console', ({ _text }) => { + res.push(_text); + }); + + setTimeout(() => { + testServer.close(() => { + // make sure the client gets the close message + setTimeout(() => { + browser.close().then(() => { + expect(res).toMatchSnapshot(); + done(); + }); + }, 1000); + }); + }, 3000); + }); + }); + }); + }); + + describe('custom client', () => { + beforeAll((done) => { + const options = { + port, + host: '0.0.0.0', + inline: true, + clientMode: require.resolve( + '../fixtures/custom-client/CustomSockJSClient' + ), + }; + testServer.startAwaitingCompilation(config, options, done); + }); + + describe('on browser client', () => { + it('logs additional messages to console', (done) => { + runBrowser().then(({ page, browser }) => { + const res = []; + page.goto(`http://localhost:${port}/main`); + page.on('console', ({ _text }) => { + res.push(_text); + }); + + setTimeout(() => { + testServer.close(() => { + // make sure the client gets the close message + setTimeout(() => { + browser.close().then(() => { + expect(res).toMatchSnapshot(); + done(); + }); + }, 1000); + }); + }, 3000); + }); + }); + }); + }); +}); diff --git a/test/e2e/__snapshots__/ClientMode.test.js.snap b/test/e2e/__snapshots__/ClientMode.test.js.snap new file mode 100644 index 0000000000..e7517d8b16 --- /dev/null +++ b/test/e2e/__snapshots__/ClientMode.test.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`clientMode custom client on browser client logs additional messages to console 1`] = ` +Array [ + "Hey.", + "open", + "liveReload", + "[WDS] Live Reloading enabled.", + "hash", + "ok", + "close", + "[WDS] Disconnected!", +] +`; + +exports[`clientMode sockjs on browser client logs as usual 1`] = ` +Array [ + "Hey.", + "[WDS] Live Reloading enabled.", + "[WDS] Disconnected!", +] +`; diff --git a/test/fixtures/custom-client/CustomSockJSClient.js b/test/fixtures/custom-client/CustomSockJSClient.js new file mode 100644 index 0000000000..8412abeba7 --- /dev/null +++ b/test/fixtures/custom-client/CustomSockJSClient.js @@ -0,0 +1,41 @@ +'use strict'; + +/* eslint-disable + no-unused-vars +*/ +const SockJS = require('sockjs-client/dist/sockjs'); +const BaseClient = require('../../../client/clients/BaseClient'); + +module.exports = class SockJSClient extends BaseClient { + constructor(url) { + super(); + this.sock = new SockJS(url); + } + + static getClientPath(options) { + return require.resolve('./CustomSockJSClient'); + } + + onOpen(f) { + this.sock.onopen = () => { + console.log('open'); + f(); + }; + } + + onClose(f) { + this.sock.onclose = () => { + console.log('close'); + f(); + }; + } + + // call f with the message string as the first argument + onMessage(f) { + this.sock.onmessage = (e) => { + const obj = JSON.parse(e.data); + console.log(obj.type); + f(e.data); + }; + } +}; diff --git a/test/options.test.js b/test/options.test.js index 7e59879bb0..3ba18e17f7 100644 --- a/test/options.test.js +++ b/test/options.test.js @@ -137,6 +137,10 @@ describe('options', () => { ], failure: ['whoops!'], }, + clientMode: { + success: ['sockjs', require.resolve('../client/clients/SockJSClient')], + failure: [false], + }, compress: { success: [true], failure: [''], diff --git a/test/ports-map.js b/test/ports-map.js index d8db8aab20..5031f898be 100644 --- a/test/ports-map.js +++ b/test/ports-map.js @@ -1,6 +1,7 @@ 'use strict'; // test-file-name: the number of ports +// important: new port mappings must be added to the bottom of this list const portsList = { cli: 2, sockJSClient: 1, @@ -37,6 +38,7 @@ const portsList = { ProvidePlugin: 1, WebsocketClient: 1, WebsocketServer: 1, + ClientMode: 1, }; let startPort = 8079; diff --git a/test/server/utils/getSocketClientPath.test.js b/test/server/utils/getSocketClientPath.test.js new file mode 100644 index 0000000000..4eef174189 --- /dev/null +++ b/test/server/utils/getSocketClientPath.test.js @@ -0,0 +1,50 @@ +'use strict'; + +const getSocketClientPath = require('../../../lib/utils/getSocketClientPath'); +// 'npm run prepare' must be done for this test to pass +const sockjsClientPath = require.resolve( + '../../../client/clients/SockJSClient' +); +const baseClientPath = require.resolve('../../../client/clients/BaseClient'); + +describe('getSocketClientPath', () => { + it("should work with clientMode: 'sockjs'", () => { + let result; + + expect(() => { + result = getSocketClientPath({ + clientMode: 'sockjs', + }); + }).not.toThrow(); + + expect(result).toEqual(sockjsClientPath); + }); + + it('should work with clientMode: SockJSClient full path', () => { + let result; + + expect(() => { + result = getSocketClientPath({ + clientMode: sockjsClientPath, + }); + }).not.toThrow(); + + expect(result).toEqual(sockjsClientPath); + }); + + it('should throw with clientMode: bad path', () => { + expect(() => { + getSocketClientPath({ + clientMode: '/bad/path/to/implementation', + }); + }).toThrow(/clientMode must be a string/); + }); + + it('should throw with clientMode: unimplemented client', () => { + expect(() => { + getSocketClientPath({ + clientMode: baseClientPath, + }); + }).toThrow('Client needs implementation'); + }); +});