diff --git a/packages/metro/src/lib/bundle-modules/HMRClient.js b/packages/metro/src/lib/bundle-modules/HMRClient.js index e06c22b3..d5b5597a 100644 --- a/packages/metro/src/lib/bundle-modules/HMRClient.js +++ b/packages/metro/src/lib/bundle-modules/HMRClient.js @@ -9,92 +9,4 @@ */ 'use strict'; -const EventEmitter = require('eventemitter3'); - -/** - * The Hot Module Reloading Client connects to metro via Websockets, to receive - * updates from it and propagate them to the runtime to reflect the changes. - */ -class HMRClient extends EventEmitter { - _wsClient: ?WebSocket; - _url: string; - - constructor(url: string) { - super(); - - this._url = url; - } - - enable() { - if (this._wsClient) { - this.disable(); - } - - // Access the global WebSocket object only after enabling the client, - // since some polyfills do the initialization lazily. - const WSConstructor = global.WebSocket; - - // create the WebSocket connection. - this._wsClient = new WSConstructor(this._url); - - this._wsClient.onerror = e => { - this.emit('connection-error', e); - }; - - this._wsClient.onmessage = message => { - const data = JSON.parse(message.data); - - switch (data.type) { - case 'update-start': - this.emit('update-start'); - break; - - case 'update': - const {modules, sourceMappingURLs, sourceURLs} = data.body; - - this.emit('update'); - - modules.forEach(({id, code}, i) => { - code += '\n\n' + sourceMappingURLs[i]; - - // on JSC we need to inject from native for sourcemaps to work - // (Safari doesn't support `sourceMappingURL` nor any variant when - // evaluating code) but on Chrome we can simply use eval - const injectFunction = - typeof global.nativeInjectHMRUpdate === 'function' - ? global.nativeInjectHMRUpdate - : eval; // eslint-disable-line no-eval - - injectFunction(code, sourceURLs[i]); - }); - break; - - case 'update-done': - this.emit('update-done'); - break; - - case 'error': - this.emit('error', { - type: data.body.type, - message: data.body.message, - }); - break; - - default: - this.emit('error', {type: 'unknown-message', message: data}); - } - }; - } - - disable() { - if (!this._wsClient) { - return; - } - - this._wsClient.close(); - - this._wsClient = undefined; - } -} - -module.exports = HMRClient; +module.exports = require('./MetroClient'); diff --git a/packages/metro/src/lib/bundle-modules/MetroClient.js b/packages/metro/src/lib/bundle-modules/MetroClient.js new file mode 100644 index 00000000..5e7c55fc --- /dev/null +++ b/packages/metro/src/lib/bundle-modules/MetroClient.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ +'use strict'; + +const EventEmitter = require('eventemitter3'); + +/** + * The Hot Module Reloading Client connects to Metro via WebSocket, to receive + * updates from it and propagate them to the runtime to reflect the changes. + */ +class MetroClient extends EventEmitter { + _ws: ?WebSocket; + _url: string; + + constructor(url: string) { + super(); + this._url = url; + } + + enable() { + if (this._ws) { + this.disable(); + } + + // Access the global WebSocket object only after enabling the client, + // since some polyfills do the initialization lazily. + this._ws = new global.WebSocket(this._url); + this._ws.onerror = error => { + this.emit('connection-error', error); + }; + this._ws.onmessage = message => { + const data = JSON.parse(message.data); + switch (data.type) { + case 'update-start': + this.emit('update-start'); + break; + + case 'update': + const {modules, sourceMappingURLs, sourceURLs} = data.body; + + this.emit('update'); + modules.forEach(({id, code}, i) => { + code += '\n\n' + sourceMappingURLs[i]; + + // In JSC we need to inject from native for sourcemaps to work + // (Safari doesn't support `sourceMappingURL` nor any variant when + // evaluating code) but on Chrome we can simply use eval. + const injectFunction = + typeof global.nativeInjectHMRUpdate === 'function' + ? global.nativeInjectHMRUpdate + : eval; // eslint-disable-line no-eval + + injectFunction(code, sourceURLs[i]); + }); + break; + + case 'update-done': + this.emit('update-done'); + break; + + case 'error': + this.emit('error', { + type: data.body.type, + message: data.body.message, + }); + break; + + default: + this.emit('error', {type: 'unknown-message', message: data}); + } + }; + } + + disable() { + if (this._ws) { + this._ws.close(); + this._ws = undefined; + } + } +} + +module.exports = MetroClient; diff --git a/packages/metro/src/lib/bundle-modules/__tests__/MetroClient-test.js b/packages/metro/src/lib/bundle-modules/__tests__/MetroClient-test.js new file mode 100644 index 00000000..e585a0a4 --- /dev/null +++ b/packages/metro/src/lib/bundle-modules/__tests__/MetroClient-test.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @emails oncall+js_foundation + * @format + */ +'use strict'; + +const MetroClient = require('../MetroClient'); + +let mockSocket = null; +global.WebSocket = jest.fn(() => { + mockSocket = { + onerror: jest.fn(), + onmessage: jest.fn(), + close: jest.fn(), + mockEmit: (type, data) => { + if (mockSocket) { + if (type === 'error') { + mockSocket.onerror(data); + } else { + mockSocket.onmessage(data); + } + } + }, + }; + return mockSocket; +}); + +beforeEach(() => (mockSocket = null)); + +test('connects to a WebSocket and listens to messages', () => { + const client = new MetroClient('wss://banana.com/phone'); + + const mockError = { + message: 'An error occurred.', + }; + const mockErrorCallback = jest.fn(data => expect(data).toEqual(mockError)); + const mockUpdateStartCallback = jest.fn(); + + expect(mockSocket).toBeNull(); + client.on('connection-error', mockErrorCallback); + client.on('update-start', mockUpdateStartCallback); + client.enable(); + if (!mockSocket) { + throw new Error('mockSocket was not set when opening the connection.'); + } + + mockSocket.mockEmit('message', { + data: JSON.stringify({ + type: 'update-start', + }), + }); + + expect(mockUpdateStartCallback).toBeCalled(); + + mockSocket.mockEmit('error', mockError); + expect(mockErrorCallback).toBeCalled(); + + expect(mockSocket.close).not.toBeCalled(); + client.disable(); + expect(mockSocket.close).toBeCalled(); +});