diff --git a/client-src/default/socket.js b/client-src/default/socket.js index adf71b5969..4c8bcbba20 100644 --- a/client-src/default/socket.js +++ b/client-src/default/socket.js @@ -1,24 +1,28 @@ 'use strict'; -const SockJS = require('sockjs-client/dist/sockjs'); +/* global __webpack_dev_server_client__ */ +/* eslint-disable + camelcase +*/ +const Client = __webpack_dev_server_client__; let retries = 0; -let sock = null; +let client = null; const socket = function initSocket(url, handlers) { - sock = new SockJS(url); + client = new Client(url); - sock.onopen = function onopen() { + client.onOpen(() => { retries = 0; - }; + }); - sock.onclose = function onclose() { + client.onClose(() => { if (retries === 0) { handlers.close(); } // Try to reconnect. - sock = null; + client = null; // After 10 retries stop trying, to prevent logspam. if (retries <= 10) { @@ -32,15 +36,14 @@ const socket = function initSocket(url, handlers) { socket(url, handlers); }, retryInMs); } - }; + }); - sock.onmessage = function onmessage(e) { - // This assumes that all data sent via the websocket is JSON. - const msg = JSON.parse(e.data); + client.onMessage((data) => { + const msg = JSON.parse(data); if (handlers[msg.type]) { handlers[msg.type](msg.data); } - }; + }); }; module.exports = socket; diff --git a/lib/clients/BaseClient.js b/lib/clients/BaseClient.js new file mode 100644 index 0000000000..1dfa65911b --- /dev/null +++ b/lib/clients/BaseClient.js @@ -0,0 +1,10 @@ +'use strict'; + +/* eslint-disable + no-unused-vars +*/ +module.exports = class BaseClient { + static getClientPath(options) { + throw new Error('Client needs implementation'); + } +}; diff --git a/lib/clients/SockJSClient.js b/lib/clients/SockJSClient.js new file mode 100644 index 0000000000..388c25fef1 --- /dev/null +++ b/lib/clients/SockJSClient.js @@ -0,0 +1,33 @@ +'use strict'; + +/* eslint-disable + no-unused-vars +*/ +const SockJS = require('sockjs-client/dist/sockjs'); +const BaseClient = require('./BaseClient'); + +module.exports = class SockJSClient extends BaseClient { + constructor(url) { + super(); + this.sock = new SockJS(url); + } + + static getClientPath(options) { + return require.resolve('./SockJSClient'); + } + + onOpen(f) { + this.sock.onopen = f; + } + + onClose(f) { + this.sock.onclose = f; + } + + // call f with the message string as the first argument + onMessage(f) { + this.sock.onmessage = (e) => { + f(e.data); + }; + } +}; diff --git a/lib/clients/WebsocketClient.js b/lib/clients/WebsocketClient.js new file mode 100644 index 0000000000..1ae8ecf2fb --- /dev/null +++ b/lib/clients/WebsocketClient.js @@ -0,0 +1,5 @@ +'use strict'; + +const BaseClient = require('./BaseClient'); + +module.exports = class WebsocketClient extends BaseClient {}; diff --git a/lib/utils/updateCompiler.js b/lib/utils/updateCompiler.js index 4f6619874a..28fa629b9e 100644 --- a/lib/utils/updateCompiler.js +++ b/lib/utils/updateCompiler.js @@ -6,6 +6,7 @@ */ const webpack = require('webpack'); const addEntries = require('./addEntries'); +const SockJSClient = require('./../clients/SockJSClient'); function updateCompiler(compiler, options) { if (options.inline !== false) { @@ -48,6 +49,11 @@ function updateCompiler(compiler, options) { compilers.forEach((compiler) => { const config = compiler.options; compiler.hooks.entryOption.call(config.context, config.entry); + + const providePlugin = new webpack.ProvidePlugin({ + __webpack_dev_server_client__: SockJSClient.getClientPath(options), + }); + providePlugin.apply(compiler); }); // do not apply the plugin unless it didn't exist before. diff --git a/test/SockJSClient.test.js b/test/SockJSClient.test.js new file mode 100644 index 0000000000..df5c6947b6 --- /dev/null +++ b/test/SockJSClient.test.js @@ -0,0 +1,59 @@ +'use strict'; + +const http = require('http'); +const express = require('express'); +const sockjs = require('sockjs'); +const SockJSClient = require('../lib/clients/SockJSClient'); + +describe('SockJSClient', () => { + let socketServer; + let listeningApp; + + beforeAll((done) => { + // eslint-disable-next-line new-cap + const app = new express(); + listeningApp = http.createServer(app); + listeningApp.listen(8080, 'localhost', () => { + socketServer = sockjs.createServer(); + socketServer.installHandlers(listeningApp, { + prefix: '/sockjs-node', + }); + done(); + }); + }); + + describe('client', () => { + it('should open, recieve message, and close', (done) => { + socketServer.on('connection', (connection) => { + connection.write('hello world'); + setTimeout(() => { + connection.close(); + }, 1000); + }); + const client = new SockJSClient('http://localhost:8080/sockjs-node'); + const data = []; + client.onOpen(() => { + data.push('open'); + }); + client.onClose(() => { + data.push('close'); + }); + client.onMessage((msg) => { + data.push(msg); + }); + setTimeout(() => { + expect(data.length).toEqual(3); + expect(data[0]).toEqual('open'); + expect(data[1]).toEqual('hello world'); + expect(data[2]).toEqual('close'); + done(); + }, 3000); + }); + }); + + afterAll((done) => { + listeningApp.close(() => { + done(); + }); + }); +}); diff --git a/test/e2e/ProvidePlugin.test.js b/test/e2e/ProvidePlugin.test.js new file mode 100644 index 0000000000..81382937c9 --- /dev/null +++ b/test/e2e/ProvidePlugin.test.js @@ -0,0 +1,80 @@ +'use strict'; + +const testServer = require('../helpers/test-server'); +const config = require('../fixtures/provide-plugin-config/webpack.config'); +const runBrowser = require('../helpers/run-browser'); + +describe('ProvidePlugin', () => { + describe('inline', () => { + beforeAll((done) => { + const options = { + port: 9000, + host: '0.0.0.0', + inline: true, + watchOptions: { + poll: true, + }, + }; + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('on browser client', () => { + jest.setTimeout(30000); + + it('should inject SockJS client implementation', (done) => { + runBrowser().then(({ page, browser }) => { + page.waitForNavigation({ waitUntil: 'load' }).then(() => { + page + .evaluate(() => { + return window.injectedClient === window.expectedClient; + }) + .then((isCorrectClient) => { + expect(isCorrectClient).toBeTruthy(); + browser.close().then(done); + }); + }); + page.goto('http://localhost:9000/main'); + }); + }); + }); + }); + + describe('not inline', () => { + beforeAll((done) => { + const options = { + port: 9000, + host: '0.0.0.0', + inline: false, + watchOptions: { + poll: true, + }, + }; + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('on browser client', () => { + jest.setTimeout(30000); + + it('should not inject client implementation', (done) => { + runBrowser().then(({ page, browser }) => { + page.waitForNavigation({ waitUntil: 'load' }).then(() => { + page + .evaluate(() => { + // eslint-disable-next-line no-undefined + return window.injectedClient === undefined; + }) + .then((isCorrectClient) => { + expect(isCorrectClient).toBeTruthy(); + browser.close().then(done); + }); + }); + page.goto('http://localhost:9000/main'); + }); + }); + }); + }); +}); diff --git a/test/fixtures/provide-plugin-config/foo.js b/test/fixtures/provide-plugin-config/foo.js new file mode 100644 index 0000000000..c2373b58cb --- /dev/null +++ b/test/fixtures/provide-plugin-config/foo.js @@ -0,0 +1,7 @@ +'use strict'; + +const SockJSClient = require('../../../lib/clients/SockJSClient'); + +window.expectedClient = SockJSClient; +// eslint-disable-next-line camelcase, no-undef +window.injectedClient = __webpack_dev_server_client__; diff --git a/test/fixtures/provide-plugin-config/webpack.config.js b/test/fixtures/provide-plugin-config/webpack.config.js new file mode 100644 index 0000000000..50d700c09f --- /dev/null +++ b/test/fixtures/provide-plugin-config/webpack.config.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + mode: 'development', + context: __dirname, + entry: './foo.js', + output: { + path: '/', + }, + node: false, +};