Skip to content

Commit

Permalink
Merge pull request #44 from pmmmwh/feat/customize-overlay
Browse files Browse the repository at this point in the history
feat(overlay): allow full customization of the error overlay integration
  • Loading branch information
pmmmwh committed Mar 27, 2020
2 parents 0a6c114 + c42cd5b commit bd6dd94
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 30 deletions.
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),
[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.
*/

0 comments on commit bd6dd94

Please sign in to comment.