diff --git a/README.md b/README.md index 26de1887..e0277b08 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ More sample projects for common Webpack development setups are available in the > Note: If you are using TypeScript (instead of Babel) as a transpiler, you will still need to use `babel-loader` to process your source code. > Check out this [sample project](https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/examples/typescript-without-babel) on how to set this up. -### Polyfill for Older Browsers (WDS Only) +### Polyfill for Older Browsers If you need to develop on IE11, you will need to polyfill the DOM URL API. This can be done by adding the following before any of your code in the main entry (either one is fine): @@ -157,15 +157,16 @@ Modifies how the error overlay integration works in the plugin. - If an `ErrorOverlayOptions` object is provided: (**NOTE**: This is an advanced option that exists mostly for tools like `create-react-app` or `Next.js`) - - A `module` property must be defined. - It should reference a JS file that exports at least two functions with footprints as follows: + - An optional `module` property could be defined. + If it is not defined, the bundled error overlay will be used. + If defined, it should reference a JS file that exports at least two functions with footprints as follows: ```ts function handleRuntimeError(error: Error) {} function clearRuntimeErrors() {} ``` - - An optional `entry` property could also be defined, which should also reference a JS file that contains code needed to set up your custom error overlay integration. + - An optional `entry` property could be defined, which should also reference a JS file that contains code needed to set up your custom error overlay integration. If it is not defined, the bundled error overlay entry will be used. It expects the `module` file to export two more functions: @@ -186,25 +187,42 @@ Modifies how the error overlay integration works in the plugin. }; ``` -### `options.sockHost` +#### `options.overlay.sockHost` Type: `string` -Default: effectively `window.location.hostname` +Default: `window.location.hostname` -Set this if you are running webpack on a host other than `window.location.hostname`. This is used by the error overlay module. +Set this if you are running webpack on a host other than `window.location.hostname`. +This will be used by the error overlay module, and is available for `webpack-dev-server` only. -### `options.sockPort` +#### `options.overlay.sockIntegration` + +Type: `wds` or `whm` or `string` +Default: `wds` + +This controls how the error overlay connects to the sockets provided by several Webpack hot reload integrations. + +- If you use `webpack-dev-server`, you don't need to set this as it defaults to `wds`. +- If you use `webpack-hot-middleware`, you should set this to `whm`. +- If you use anything else, you will have to provide a path to a module that will accept a message handler function and initializes the socket connection. + See the [`runtime/sockets`](https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/src/runtime/sockets) folder for sample implementations. + +#### `options.overlay.sockPort` Type: `number` -Default: effectively `window.location.port` +Default: `window.location.port` -Set this if you are running webpack on a port other than `window.location.port` +Set this if you are running webpack on a port other than `window.location.port`. +This will be used by the error overlay module, and is available for `webpack-dev-server` only. -### `options.sockPath` +#### `options.overlay.sockPath` Type: `string` Default: `/sockjs-node` +Set this if you are running webpack on a custom path. +This will be used by the error overlay module, and is available for `webpack-dev-server` only. + ### `options.useLegacyWDSSockets` Type: `boolean` diff --git a/examples/webpack-hot-middleware/webpack.config.js b/examples/webpack-hot-middleware/webpack.config.js index f48d5746..832a324b 100644 --- a/examples/webpack-hot-middleware/webpack.config.js +++ b/examples/webpack-hot-middleware/webpack.config.js @@ -26,7 +26,12 @@ module.exports = { }, plugins: [ isDevelopment && new webpack.HotModuleReplacementPlugin(), - isDevelopment && new ReactRefreshPlugin(), + isDevelopment && + new ReactRefreshPlugin({ + overlay: { + sockIntegration: 'whm', + }, + }), new HtmlWebpackPlugin({ filename: './index.html', template: './public/index.html', diff --git a/src/helpers/getSocketIntegration.js b/src/helpers/getSocketIntegration.js new file mode 100644 index 00000000..0dfbc866 --- /dev/null +++ b/src/helpers/getSocketIntegration.js @@ -0,0 +1,18 @@ +function getSocketIntegration(integrationType) { + let resolvedSocketIntegration; + switch (integrationType) { + case 'wds': + resolvedSocketIntegration = require.resolve('../runtime/sockets/WDSSocket'); + break; + case 'whm': + resolvedSocketIntegration = require.resolve('../runtime/sockets/WHMEventSource'); + break; + default: + resolvedSocketIntegration = require.resolve(integrationType); + break; + } + + return resolvedSocketIntegration; +} + +module.exports = getSocketIntegration; diff --git a/src/helpers/index.js b/src/helpers/index.js index f2b7b665..b4053a9d 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -1,9 +1,11 @@ const createRefreshTemplate = require('./createRefreshTemplate'); +const getSocketIntegration = require('./getSocketIntegration'); const injectRefreshEntry = require('./injectRefreshEntry'); const validateOptions = require('./validateOptions'); module.exports = { createRefreshTemplate, + getSocketIntegration, injectRefreshEntry, validateOptions, }; diff --git a/src/helpers/validateOptions.js b/src/helpers/validateOptions.js index 1b219270..d3340e03 100644 --- a/src/helpers/validateOptions.js +++ b/src/helpers/validateOptions.js @@ -81,10 +81,11 @@ function validateOptions(options) { typeof defaultedOptions.overlay !== 'undefined' && typeof defaultedOptions.overlay !== 'boolean' ) { - const { entry, module: overlayModule, sockHost, sockPath, sockPort } = defaultedOptions.overlay; + const { entry, module: overlayModule, sockHost, sockIntegration, sockPath, sockPort } = defaultedOptions.overlay; isStringOrUndefined('overlay.entry', entry); isStringOrUndefined('overlay.module', overlayModule); isStringOrUndefined('overlay.sockHost', sockHost); + isStringOrUndefined('overlay.sockIntegration', sockIntegration); isStringOrUndefined('overlay.sockPath', sockPath); isNumberOrUndefined('overlay.sockPort', sockPort); @@ -92,6 +93,7 @@ function validateOptions(options) { ...defaultedOptions.overlay, entry: entry || defaultOverlayOptions.entry, module: overlayModule || defaultOverlayOptions.module, + sockIntegration: sockIntegration || defaultOverlayOptions.sockIntegration, }; } else { defaultedOptions.overlay = diff --git a/src/index.js b/src/index.js index 0b2c38d3..7026c659 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,12 @@ const path = require('path'); const webpack = require('webpack'); -const { createRefreshTemplate, injectRefreshEntry, validateOptions } = require('./helpers'); -const { errorOverlay, refreshUtils } = require('./runtime/globals'); +const { + createRefreshTemplate, + getSocketIntegration, + injectRefreshEntry, + validateOptions, +} = require('./helpers'); +const { errorOverlay, initSocket, refreshUtils } = require('./runtime/globals'); class ReactRefreshPlugin { /** @@ -9,7 +14,7 @@ class ReactRefreshPlugin { * @returns {void} */ constructor(options) { - this.options = validateOptions(options); + this.options = validateOptions(options || {}); } /** @@ -39,6 +44,7 @@ class ReactRefreshPlugin { [refreshUtils]: require.resolve('./runtime/refreshUtils'), ...(!!this.options.overlay && { [errorOverlay]: require.resolve(this.options.overlay.module), + [initSocket]: getSocketIntegration(this.options.overlay.sockIntegration), }), }); providePlugin.apply(compiler); diff --git a/src/runtime/ErrorOverlayEntry.js b/src/runtime/ErrorOverlayEntry.js index 72330286..de81f879 100644 --- a/src/runtime/ErrorOverlayEntry.js +++ b/src/runtime/ErrorOverlayEntry.js @@ -1,11 +1,10 @@ -/* global __resourceQuery, __react_refresh_error_overlay__ */ +/* global __resourceQuery, __react_refresh_error_overlay__, __react_refresh_init_socket__ */ -const formatWebpackErrors = require('./formatWebpackErrors'); -const createSocket = require('./createSocket'); const { error: registerErrorHandler, unhandledRejection: registerUnhandledRejectionHandler, } = require('./errorEventHandlers'); +const formatWebpackErrors = require('./formatWebpackErrors'); // Setup error states let isHotReload = false; @@ -80,7 +79,7 @@ if (__resourceQuery) { } // Registers handlers for compile errors -createSocket(compileMessageHandler, overrides); +__react_refresh_init_socket__(compileMessageHandler, overrides); // Registers handlers for runtime errors registerErrorHandler(function handleError(error) { hasRuntimeErrors = true; diff --git a/src/runtime/LegacyWebpackDevServerSocket.js b/src/runtime/LegacyWDSSocketEntry.js similarity index 100% rename from src/runtime/LegacyWebpackDevServerSocket.js rename to src/runtime/LegacyWDSSocketEntry.js diff --git a/src/runtime/WHMEventSource.js b/src/runtime/WHMEventSource.js deleted file mode 100644 index 47390378..00000000 --- a/src/runtime/WHMEventSource.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * If the consumers setup is to use webpack-hot-middleware with a custom express server - * we want to bind onto the EventSource for error tracking - */ - -module.exports = function loadWHMEventSource(messageHandler) { - const client = require('webpack-hot-middleware/client'); - - client.useCustomOverlay({ - showProblems(type, data) { - const error = { - type, - data, - }; - - messageHandler(error); - }, - clear() { - messageHandler({ type: 'ok' }); - }, - }); -}; diff --git a/src/runtime/createSocket.js b/src/runtime/createSocket.js deleted file mode 100644 index af9ad992..00000000 --- a/src/runtime/createSocket.js +++ /dev/null @@ -1,38 +0,0 @@ -// eslint-disable-next-line no-unused-vars -/* global __resourceQuery, __webpack_dev_server_client__ */ - -const url = require('native-url'); -const loadWHMEventSource = require('./WHMEventSource'); - -/** - * Creates a socket server for HMR according to the user's Webpack configuration. - * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. - */ -function createSocket(messageHandler, options) { - // This adds support for custom WDS socket transportModes - // In the future, we should add support for custom clients to better support WDM - if (typeof __webpack_dev_server_client__ !== 'undefined') { - const SocketClient = __webpack_dev_server_client__; - const connection = new SocketClient( - url.format({ - protocol: window.location.protocol, - hostname: options.sockHost || window.location.hostname, - port: options.sockPort || window.location.port, - // TODO: Support usage of custom sockets after WDS 4.0 is released - // Ref: https://github.com/webpack/webpack-dev-server/pull/2055 - pathname: options.sockPath || '/sockjs-node', - }) - ); - connection.onClose(function onSocketClose() { - // TODO: Should we reconnect? - }); - connection.onMessage(function onSocketMessage(data) { - const message = JSON.parse(data); - messageHandler(message); - }); - } else { - loadWHMEventSource(messageHandler); - } -} - -module.exports = createSocket; diff --git a/src/runtime/globals.js b/src/runtime/globals.js index 47621a5b..f4020c25 100644 --- a/src/runtime/globals.js +++ b/src/runtime/globals.js @@ -1,3 +1,5 @@ -module.exports.errorOverlay = '__react_refresh_error_overlay__'; - -module.exports.refreshUtils = '__react_refresh_utils__'; +module.exports = { + errorOverlay: '__react_refresh_error_overlay__', + initSocket: '__react_refresh_init_socket__', + refreshUtils: '__react_refresh_utils__', +}; diff --git a/src/runtime/sockets/WDSSocket.js b/src/runtime/sockets/WDSSocket.js new file mode 100644 index 00000000..cb3fad89 --- /dev/null +++ b/src/runtime/sockets/WDSSocket.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line no-unused-vars +/* global __resourceQuery, __webpack_dev_server_client__ */ + +const url = require('native-url'); + +/** + * Initializes a socket server for HMR according to the user's Webpack configuration. + * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. + * @param {*} overrides Socket integration overrides to change the connection URL. + * @returns {void} + */ +function initWDSSocket(messageHandler, overrides) { + if (typeof __webpack_dev_server_client__ !== 'undefined') { + const SocketClient = __webpack_dev_server_client__; + // TODO: Support usage of custom sockets after WDS 4.0 is released + // Ref: https://github.com/webpack/webpack-dev-server/pull/2055 + const connection = new SocketClient( + url.format({ + protocol: window.location.protocol, + hostname: overrides.sockHost || window.location.hostname, + port: overrides.sockPort || window.location.port, + pathname: overrides.sockPath || '/sockjs-node', + }) + ); + + connection.onMessage(function onSocketMessage(data) { + const message = JSON.parse(data); + messageHandler(message); + }); + } +} + +module.exports = initWDSSocket; diff --git a/src/runtime/sockets/WHMEventSource.js b/src/runtime/sockets/WHMEventSource.js new file mode 100644 index 00000000..1665a6e6 --- /dev/null +++ b/src/runtime/sockets/WHMEventSource.js @@ -0,0 +1,37 @@ +/* + * If the consumers' setup is to use webpack-hot-middleware with a custom express server, + * we want to bind onto the EventSource for error tracking. + */ + +/** + * The hard-coded singleton key for webpack-hot-middleware's client instance. + * + * [Ref](https://github.com/webpack-contrib/webpack-hot-middleware/blob/cb29abb9dde435a1ac8e9b19f82d7d36b1093198/client.js#L152) + */ +const singletonKey = '__webpack_hot_middleware_reporter__'; + +/** + * Creates a socket server for HMR according to the user's Webpack configuration. + * @param {function(*): void} messageHandler A handler to consume Webpack compilation messages. + * @param {*} overrides Socket integration overrides to change the connection URL. + * @returns {void} + */ +function initWHMEventSource(messageHandler, overrides) { + const client = window[singletonKey] || require('webpack-hot-middleware/client'); + + client.useCustomOverlay({ + showProblems(type, data) { + const error = { + type, + data, + }; + + messageHandler(error); + }, + clear() { + messageHandler({ type: 'ok' }); + }, + }); +} + +module.exports = initWHMEventSource; diff --git a/src/types.js b/src/types.js index 809f343d..830a4390 100644 --- a/src/types.js +++ b/src/types.js @@ -3,6 +3,7 @@ * @property {string} [entry] Path to a JS file that sets up the error overlay integration. * @property {string} [module] The error overlay module to use. * @property {string} [sockHost] The socket host to use. + * @property {'wds' | 'whm' | string} [sockIntegration] Path to a JS file that sets up the Webpack socket integration. * @property {string} [sockPath] The socket path to use. * @property {number} [sockPort] The socket port to use. */