Skip to content
This repository has been archived by the owner on Sep 7, 2023. It is now read-only.

Commit

Permalink
feat: major rewrite, separated loader & runtime from webpack plugin
Browse files Browse the repository at this point in the history
- min webpack version bumped to v5.0.0-beta.15
- added support for module.hot.invalidate
- updated configuration instructions
- added support for descriptor entries
- added optional plugin for embedding react-error-overlay
  • Loading branch information
apostolos committed May 11, 2020
1 parent a5d72f3 commit 4169bf7
Show file tree
Hide file tree
Showing 20 changed files with 601 additions and 536 deletions.
3 changes: 1 addition & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"$schema": "http://json.schemastore.org/prettierrc",
"singleQuote": true,
"trailingComma": "es5"
"singleQuote": true
}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## 5.0.0 (May 10th, 2020)

We no longer want to assume anything about your webpack configuration (e.g. file extensions for your React components). We also want to use as little as possible of webpack's internal API.

For this reason, we no longer trying to put everything together for you. The loader and entry runtime have been separated from the webpack plugin. Users will need to manually add each one to their configuration. Besides significantly simplifying our code, this change will help keep compatibility with future webpack versions and with more advanced/obscure configurations.

Version was bumped to v5 in order to match minimum compatible webpack version.

- **BREAKING CHANGE**: Minimum required version is now `webpack@v5.0.0-beta.15`
- **BREAKING CHANGE**: Separated the webpack loader and entry runtime from the webpack plugin. See our [README](./README.md) for instructions.
- **NEW**: Added optional plugin for integration with [react-error-overlay](https://github.com/facebook/create-react-app/tree/master/packages/react-error-overlay)
- **NEW**: Added support for `module.hot.invalidate`
- **FIX**: We now properly support webpack's [descriptor entries](https://webpack.js.org/configuration/entry-context/#entry-descriptor)

## 2.0.0 (Mar 21st, 2020)

- Now requires Babel 7.9.0+ with runtime=`automatic` on preset-react (see [README.md](./README.md))
Expand Down
145 changes: 137 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,152 @@
# webpack-fast-refresh

React Fast Refresh plugin and loader for webpack@5+ and babel@7.9.0+
React Fast Refresh for `webpack@5+` and `babel@7.9+`

webpack@4 users should try https://github.com/pmmmwh/react-refresh-webpack-plugin

# Usage

## 1. Install both `react-refresh` and `webpack-fast-refresh` in your project
## 1. Install both `react-refresh` and `webpack-fast-refresh`

```bash
yarn add -D -E @webhotelier/webpack-fast-refresh react-refresh
# or
npm install -D -E @webhotelier/webpack-fast-refresh react-refresh
# or
yarn add -D -E @webhotelier/webpack-fast-refresh react-refresh
```

## 2. Configure webpack

Register the plugin in `webpack.config.js`:
Make the following changes to your `webpack.config.js`:

```javascript
### a) Register the plugin:

```js
const ReactRefreshPlugin = require('@webhotelier/webpack-fast-refresh');

config.plugins.unshift(new ReactRefreshPlugin());

// or if you have an object-based config:
{
...otherSettings,
plugins: [new ReactRefreshPlugin(), ...otherplugins];
}
```

### b) Place the runtime in front of your entrypoint:

Depending on how you have configured your entry, change it similarly to the following examples:

```js
// if it looks like this ("./index.js" is just an example, can be any file or path)
config.entry = './index.js'; // or
config.entry = ['./index.js'];
// change it to this:
config.entry = ['@webhotelier/webpack-fast-refresh/runtime.js', './index.js'];

// if it looks like this
config.entry = {
import: './index.js',
}; // or
config.entry = {
import: ['./index.js'],
};
// change it to this:
config.entry = {
import: ['@webhotelier/webpack-fast-refresh/runtime.js', './index.js'],
};

// named entry points are also supported ("main" is just an example, can be any entry name)
config.main.entry = './index.js'; // or
config.main.entry = ['./index.js'];
// change to:
config.main.entry = [
'@webhotelier/webpack-fast-refresh/runtime.js',
'./index.js',
];

// Examples of object-based config:
// change:
{
"entry": {
"main": "./index.js"
}
}

// to:
{
"entry": {
"main": ["@webhotelier/webpack-fast-refresh/runtime.js", "./index.js"]
}
}
```

### c) Place the loader in your rule matching React files:

Let's say you have the following rule:

```js
{
"rules": [
{
"test": /\.jsx$/,
"use": [
{ "loader": "babel-loader", "options": { "cacheDirectory": true } }
]
}
]
}
```

Change to:

```json
{
"module": {
"rules": [
{
"test": /\.jsx$/,
"use": [
{ "loader": "babel-loader", "options": { "cacheDirectory": true } },
{ "loader": "@webhotelier/webpack-fast-refresh/loader.js" }
]
}
]
}
}
```

or push it with code:

```js
// make sure to use the index of your JSX loader, 0 in this example
config.module.rules[0].use.push('@webhotelier/webpack-fast-refresh/loader.js');
```

## 3. Configure babel

Add react-refresh/babel to your babelrc:

```json
{
"presets": [["@babel/preset-react", { "runtime": "automatic" }]],
"plugins": ["react-refresh/babel"]
}
```

## 4. Launch the server
## 4. Configure error-overlay plugin (optional)

```js
const ErrorOverlayPlugin = require('@webhotelier/webpack-fast-refresh/error-overlay');
config.plugins.push(new ErrorOverlayPlugin());

// or if you have an object-based config:
{
...otherSettings,
plugins: [new ErrorOverlayPlugin(), ...otherplugins];
}
```

## 5. Launch the server

Make sure you have [HMR](https://webpack.js.org/concepts/hot-module-replacement/) enabled.

Expand Down Expand Up @@ -78,7 +192,22 @@ if (app.get('env') === 'development') {
}
```

# Common Issues

## Production problems

The above plugins/loader/etc are not checking if running in production builds.

Make sure you add the correct checks to only include them in development builds.

## Still having trouble configuring everything?

Real-world example using the plugin:

https://github.com/LWJGL/lwjgl3-www/blob/master/webpack.config.cjs

# References

- [Fork of @pmmmwh/react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin)
- [@next/react-refresh-utils](https://github.com/zeit/next.js/tree/canary/packages/react-refresh-utils)
- [@pmmmwh/react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin)
- [Implementation by @maisano](https://gist.github.com/maisano/441a4bc6b2954205803d68deac04a716)
34 changes: 34 additions & 0 deletions error-overlay/entry-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//@ts-check
const {
setEditorHandler,
startReportingRuntimeErrors,
dismissRuntimeErrors,
} = require('react-error-overlay');
const launchEditorEndpoint = require('react-dev-utils/launchEditorEndpoint');

setEditorHandler((errorLocation) => {
// Keep this sync with errorOverlayMiddleware.js
fetch(
launchEditorEndpoint +
'?fileName=' +
window.encodeURIComponent(errorLocation.fileName) +
'&lineNumber=' +
window.encodeURIComponent(errorLocation.lineNumber || 1) +
'&colNumber=' +
window.encodeURIComponent(errorLocation.colNumber || 1)
);
});

startReportingRuntimeErrors({
onError() {
//@ts-ignore
if (module.hot) {
module.hot.addStatusHandler((status) => {
if (status === 'apply') {
// window.location.reload();
dismissRuntimeErrors();
}
});
}
},
});
38 changes: 38 additions & 0 deletions error-overlay/entry-devserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const querystring = require('querystring');
const url = require('url');
const SockJS = require('sockjs-client');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const { reportBuildError, dismissBuildError } = require('react-error-overlay');

let sockOptions = {};
if (typeof __resourceQuery === 'string' && __resourceQuery.length > 1) {
sockOptions = querystring.parse(__resourceQuery.substr(1));
}

const connection = new SockJS(
url.format({
protocol: window.location.protocol,
hostname: sockOptions.sockHost || window.location.hostname,
port: sockOptions.sockPort || window.location.port,
pathname: sockOptions.sockPath || '/sockjs-node',
})
);

connection.onmessage = function onmessage(e) {
const { type, data } = JSON.parse(e.data);
let formatted;
switch (type) {
case 'ok':
dismissBuildError();
break;
case 'errors':
formatted = formatWebpackMessages({
errors: data,
warnings: [],
});
reportBuildError(formatted.errors[0]);
break;
default:
// Do nothing.
}
};
107 changes: 107 additions & 0 deletions error-overlay/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//@ts-check
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');

const chunkPathBasic = require.resolve('./entry-basic.js');
const chunkPathDevServer = require.resolve('./entry-devserver.js');
let deps = [];

/** @typedef {{ sockHost: string, sockPath: string, sockPort: number }} SockOptions */
/** @typedef {{ dependOn?: [string, ...string[]], filename?: any, import: [string, ...string[]], library?: any }} EntryDescriptionNormalized */

class ErrorOverlayPlugin {
apply(/** @type { import("webpack").Compiler } */ compiler) {
const className = this.constructor.name;

if (compiler.options.mode !== 'development') {
return;
}

const devServerEnabled = !!compiler.options.devServer;
const usingSocket = typeof compiler.options.devServer.socket === 'string';

/** @type { SockOptions } */
const sockOptions = {};
if (devServerEnabled && usingSocket) {
sockOptions.sockHost = compiler.options.devServer.sockHost;
sockOptions.sockPath = compiler.options.devServer.sockPath;
sockOptions.sockPort = compiler.options.devServer.sockPort;
}

//@ts-ignore
compiler.hooks.entryOption.tap(className, (context, entry) => {
if (typeof entry !== 'function') {
Object.keys(entry).forEach((entryName) => {
if (deps.includes(entryName)) {
// Skip dependencies, only inject real entry points
return;
}
entry[entryName] = adjustEntry(
entry[entryName],
devServerEnabled,
usingSocket,
sockOptions
);
});
}
});

compiler.hooks.afterResolvers.tap(className, ({ options }) => {
if (devServerEnabled) {
const originalBefore = options.devServer.before;
options.devServer.before = (app, server) => {
if (originalBefore) {
originalBefore(app, server);
}
app.use(errorOverlayMiddleware());
};
}
});
}
}

/**
* Puts dev server chunk path in front of other entries
* @param {EntryDescriptionNormalized} entry
* @param {boolean} enableDevServer
* @param {boolean} usingSocket
* @param {SockOptions} sockOptions
* @returns {EntryDescriptionNormalized}
*/
function adjustEntry(entry, enableDevServer, usingSocket, sockOptions) {
if (entry.dependOn) {
deps = [...deps, ...entry.dependOn];
}

if (entry.library) {
// skip libraries
return entry;
}

if (typeof entry.import === 'string') {
entry.import = [entry.import];
}

if (enableDevServer && usingSocket) {
const sockHost = sockOptions.sockHost
? `&sockHost=${sockOptions.sockHost}`
: '';
const sockPath = sockOptions.sockPath
? `&sockPath=${sockOptions.sockPath}`
: '';
const sockPort = sockOptions.sockPort
? `&sockPort=${sockOptions.sockPort}`
: '';
const chunkPathDevServerWithParams = `${chunkPathDevServer}?${sockHost}${sockPath}${sockPort}`;
if (!entry.import.includes(chunkPathDevServerWithParams)) {
entry.import.unshift(chunkPathDevServerWithParams);
}
}

if (!entry.import.includes(chunkPathBasic)) {
entry.import.unshift(chunkPathBasic);
}

return entry;
}

module.exports = ErrorOverlayPlugin;
Loading

0 comments on commit 4169bf7

Please sign in to comment.