Skip to content

feat(overlay): allow full customization of the error overlay integration #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 27, 2020
70 changes: 64 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,71 @@ module.exports = api => {
## Options

This plugin accepts a few options that are specifically targeted for advanced users.
The allowed values are as follows:

| Name | Type | Default | Description |
| :-----------------------: | :-------: | :-----: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`disableRefreshCheck`** | `boolean` | `false` | Disables detection of react-refresh's Babel plugin. Useful if you do not parse JS files within `node_modules`, or if you have a Babel setup not entirely controlled by Webpack. |
| **`forceEnable`** | `boolean` | `false` | Enables the plugin forcefully. Useful if you want to use the plugin in production, or if you are using Webpack's `none` mode without `NODE_ENV`, for example. |
| **`useLegacyWDSSockets`** | `boolean` | `false` | Set this to true if you are using a webpack-dev-server version prior to 3.8 as it requires a custom SocketJS implementation. If you use this, you will also need to install `sockjs-client` as a peer depencency. |
### `options.disableRefreshCheck`

Type: `boolean`
Default: `false`

Disables detection of react-refresh's Babel plugin.
Useful if you do not parse JS files within `node_modules`, or if you have a Babel setup not entirely controlled by Webpack.

### `options.forceEnable`

Type: `boolean`
Default: `false`

Enables the plugin forcefully.
Useful if you want to use the plugin in production, or if you are using Webpack's `none` mode without `NODE_ENV`, for example.

### `options.overlay`

Type: `boolean | ErrorOverlayOptions`
Default: `undefined`

Modifies how the error overlay integration works in the plugin.

- If `options.overlay` is not provided or is `true`, the plugin will use the bundled error overlay interation.
- If `options.overlay` is `false`, it will disable the error overlay integration.
- 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:

```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.
If it is not defined, the bundled error overlay entry will be used.
It expects the `module` file to export two more functions:

```ts
function showCompileError(webpackErrorMessage: string) {}
function clearCompileErrors() {}
```

Note that `webpackErrorMessage` is ANSI encoded, so you will need logic to parse it.

- An example configuration:
```js
const options = {
overlay: {
entry: 'some-webpack-entry-file',
module: 'some-error-overlay-module',
},
};
```

### `options.useLegacyWDSSockets`

Type: `boolean`
Default: `false`

Set this to true if you are using a `webpack-dev-server` version prior to 3.8 as it requires a custom SockJS implementation.
If you use this feature, you will also need to install `sockjs-client` as a peer dependency.

## Related Work

Expand Down
2 changes: 2 additions & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const createRefreshTemplate = require('./createRefreshTemplate');
const injectRefreshEntry = require('./injectRefreshEntry');
const validateOptions = require('./validateOptions');

module.exports = {
createRefreshTemplate,
injectRefreshEntry,
validateOptions,
};
5 changes: 3 additions & 2 deletions src/helpers/injectRefreshEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
/**
* Injects an entry to the bundle for react-refresh.
* @param {WebpackEntry} [originalEntry] A Webpack entry object.
* @param {ReactRefreshPluginOptions} [options] Configuration options for this plugin
* @param {import('../types').ReactRefreshPluginOptions} [options] Configuration options for this plugin.
* @returns {WebpackEntry} An injected entry object.
*/
const injectRefreshEntry = (originalEntry, options) => {
const entryInjects = [
// Legacy WDS SockJS integration
options.useLegacyWDSSockets && require.resolve('../runtime/LegacyWebpackDevServerSocket'),
// React-refresh runtime
require.resolve('../runtime/ReactRefreshEntry'),
// Error overlay runtime
require.resolve('../runtime/ErrorOverlayEntry'),
options.overlay && options.overlay.entry,
// React-refresh Babel transform detection
require.resolve('../runtime/BabelDetectComponent'),
].filter(Boolean);
Expand Down
44 changes: 44 additions & 0 deletions src/helpers/validateOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** @type {import('../types').ReactRefreshPluginOptions} */
const defaultOptions = {
disableRefreshCheck: false,
forceEnable: false,
useLegacyWDSSockets: false,
};

/** @type {import('../types').ErrorOverlayOptions} */
const defaultOverlayOptions = {
entry: require.resolve('../runtime/ErrorOverlayEntry'),
module: require.resolve('../overlay'),
};

/**
* Validates the options for the plugin.
* @param {import('../types').ReactRefreshPluginOptions} options Non-validated plugin options object.
* @returns {import('../types').ReactRefreshPluginOptions} Validated plugin options.
*/
module.exports = function validateOptions(options) {
const validatedOptions = Object.assign(defaultOptions, options);

if (
typeof validatedOptions.overlay !== 'undefined' &&
typeof validatedOptions.overlay !== 'boolean'
) {
if (typeof validatedOptions.overlay.module !== 'string') {
throw new Error(
`To use the "overlay" option, a string must be provided in the "module" property. Instead, the provided value has type: "${typeof options
.overlay.module}".`
);
}

validatedOptions.overlay = {
entry: options.overlay.entry || defaultOverlayOptions.entry,
module: options.overlay.module,
};
} else {
validatedOptions.overlay =
(typeof validatedOptions.overlay === 'undefined' || validatedOptions.overlay) &&
defaultOverlayOptions;
}

return validatedOptions;
};
25 changes: 6 additions & 19 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,19 @@
const path = require('path');
const webpack = require('webpack');
const { createRefreshTemplate, injectRefreshEntry } = require('./helpers');
const { refreshUtils } = require('./runtime/globals');

/**
* @typedef {Object} ReactRefreshPluginOptions
* @property {boolean} [disableRefreshCheck] Disables detection of react-refresh's Babel plugin.
* @property {boolean} [forceEnable] Enables the plugin forcefully.
* @property {boolean} [useLegacyWDSSockets] Uses a custom SocketJS implementation for older versions of webpack-dev-server
*/

/** @type {ReactRefreshPluginOptions} */
const defaultOptions = {
disableRefreshCheck: false,
forceEnable: false,
useLegacyWDSSockets: false,
};
const { createRefreshTemplate, injectRefreshEntry, validateOptions } = require('./helpers');
const { errorOverlay, refreshUtils } = require('./runtime/globals');

class ReactRefreshPlugin {
/**
* @param {ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
* @param {import('./types').ReactRefreshPluginOptions} [options] Options for react-refresh-plugin.
* @returns {void}
*/
constructor(options) {
this.options = Object.assign(defaultOptions, options);
this.options = validateOptions(options);
}

/**
* Applies the plugin
* Applies the plugin.
* @param {import('webpack').Compiler} compiler A webpack compiler object.
* @returns {void}
*/
Expand All @@ -50,6 +36,7 @@ class ReactRefreshPlugin {

// Inject refresh utilities to Webpack's global scope
const providePlugin = new webpack.ProvidePlugin({
[errorOverlay]: this.options.overlay && require.resolve(this.options.overlay.module),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an issue with line. Basically, this is resolving to [errorOverlay]: false so webpack is complaining that there's no module named false

image

If you remove the line, everything works fine (but of course you won't have the overlay)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I noticed this. A fix is on its way.

[refreshUtils]: require.resolve('./runtime/utils'),
});
providePlugin.apply(compiler);
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/globals.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports.errorOverlay = '__react_refresh_error_overlay__';

module.exports.refreshUtils = '__react_refresh_utils__';
10 changes: 7 additions & 3 deletions src/runtime/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global __react_refresh_error_overlay__ */
const Refresh = require('react-refresh/runtime');
const ErrorOverlay = require('../overlay');

/**
* Extracts exports from a webpack module object.
Expand Down Expand Up @@ -73,7 +73,9 @@ function createHotErrorHandler(moduleId) {
* @returns {void}
*/
function hotErrorHandler(error) {
ErrorOverlay.handleRuntimeError(error);
if (__react_refresh_error_overlay__) {
__react_refresh_error_overlay__.handleRuntimeError(error);
}
}

/**
Expand Down Expand Up @@ -109,7 +111,9 @@ function createDebounceUpdate() {
refreshTimeout = setTimeout(function() {
refreshTimeout = undefined;
Refresh.performReactRefresh();
ErrorOverlay.clearRuntimeErrors();
if (__react_refresh_error_overlay__) {
__react_refresh_error_overlay__.clearRuntimeErrors();
}
}, 30);
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @typedef {Object} ErrorOverlayOptions
* @property {string} [entry] Path to a JS file that sets up the error overlay integration.
* @property {string} module The error overlay module to use.
*/

/**
* @typedef {Object} ReactRefreshPluginOptions
* @property {boolean} [disableRefreshCheck] Disables detection of react-refresh's Babel plugin.
* @property {boolean} [forceEnable] Enables the plugin forcefully.
* @property {boolean | ErrorOverlayOptions} [overlay] Modifies how the error overlay integration works in the plugin.
* @property {boolean} [useLegacyWDSSockets] Uses a custom SocketJS implementation for older versions of webpack-dev-server.
*/