Skip to content
Next Next commit
Add react-refresh webpack plugin
  • Loading branch information
charrondev committed Mar 31, 2020
commit de556afd6ded7c09fc2e16e5e60c2e998bcd9756
149 changes: 5 additions & 144 deletions packages/react-dev-utils/webpackHotDevClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,14 @@
// This alternative WebpackDevServer combines the functionality of:
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js

// It only supports their simplest configuration (hot updates on same server).
// It makes some opinionated choices on top, like adding a syntax error overlay
// that looks similar to our console output. The error overlay is inspired by:
// The error overlay is inspired by:
// https://github.com/glenjamin/webpack-hot-middleware
// The error overlay is provided by:
// https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/src/overlay

var stripAnsi = require('strip-ansi');
var url = require('url');
var launchEditorEndpoint = require('./launchEditorEndpoint');
var formatWebpackMessages = require('./formatWebpackMessages');
var ErrorOverlay = require('react-error-overlay');

ErrorOverlay.setEditorHandler(function editorHandler(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)
);
});

// We need to keep track of if there has been a runtime error.
// Essentially, we cannot guarantee application state was not corrupted by the
// runtime error. To prevent confusing behavior, we forcibly reload the entire
// application. This is handled below when we are notified of a compile (code
// change).
// See https://github.com/facebook/create-react-app/issues/3096
var hadRuntimeError = false;
ErrorOverlay.startReportingRuntimeErrors({
onError: function() {
hadRuntimeError = true;
},
filename: '/static/js/bundle.js',
});

if (module.hot && typeof module.hot.dispose === 'function') {
module.hot.dispose(function() {
// TODO: why do we need this?
ErrorOverlay.stopReportingRuntimeErrors();
});
}

// Connect to WebpackDevServer via a socket.
var connection = new WebSocket(
Expand Down Expand Up @@ -82,106 +45,15 @@ connection.onclose = function() {
// Remember some state related to hot module replacement.
var isFirstCompilation = true;
var mostRecentCompilationHash = null;
var hasCompileErrors = false;

function clearOutdatedErrors() {
// Clean up outdated compile errors, if any.
if (typeof console !== 'undefined' && typeof console.clear === 'function') {
if (hasCompileErrors) {
console.clear();
}
}
}

// Successful compilation.
function handleSuccess() {
clearOutdatedErrors();

var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;
hasCompileErrors = false;

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onHotUpdateSuccess() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay();
});
}
}

// Compilation with warnings (e.g. ESLint).
function handleWarnings(warnings) {
clearOutdatedErrors();

var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;
hasCompileErrors = false;

function printWarnings() {
// Print warnings to the console.
var formatted = formatWebpackMessages({
warnings: warnings,
errors: [],
});

if (typeof console !== 'undefined' && typeof console.warn === 'function') {
for (var i = 0; i < formatted.warnings.length; i++) {
if (i === 5) {
console.warn(
'There were more warnings in other files.\n' +
'You can find a complete log in the terminal.'
);
break;
}
console.warn(stripAnsi(formatted.warnings[i]));
}
}
}

printWarnings();

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
tryDismissErrorOverlay();
});
}
}

// Compilation with errors (e.g. syntax error or missing modules).
function handleErrors(errors) {
clearOutdatedErrors();

isFirstCompilation = false;
hasCompileErrors = true;

// "Massage" webpack messages.
var formatted = formatWebpackMessages({
errors: errors,
warnings: [],
});

// Only show the first error.
ErrorOverlay.reportBuildError(formatted.errors[0]);

// Also log them to the console.
if (typeof console !== 'undefined' && typeof console.error === 'function') {
for (var i = 0; i < formatted.errors.length; i++) {
console.error(stripAnsi(formatted.errors[i]));
}
}

// Do not attempt to reload now.
// We will reload on next success instead.
}

function tryDismissErrorOverlay() {
if (!hasCompileErrors) {
ErrorOverlay.dismissBuildError();
tryApplyUpdates();
}
}

Expand All @@ -206,12 +78,6 @@ connection.onmessage = function(e) {
// Triggered when a file from `contentBase` changed.
window.location.reload();
break;
case 'warnings':
handleWarnings(message.data);
break;
case 'errors':
handleErrors(message.data);
break;
default:
// Do nothing.
}
Expand Down Expand Up @@ -242,12 +108,7 @@ function tryApplyUpdates(onHotUpdateSuccess) {
return;
}

function handleApplyUpdates(err, updatedModules) {
if (err || !updatedModules || hadRuntimeError) {
window.location.reload();
return;
}

function handleApplyUpdates() {
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess();
Expand Down
5 changes: 5 additions & 0 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
// @remove-on-eject-end
Expand Down Expand Up @@ -419,6 +420,7 @@ module.exports = function(webpackEnv) {
},
},
},
isEnvDevelopment && require.resolve('react-refresh/babel'),
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
Expand Down Expand Up @@ -607,6 +609,9 @@ module.exports = function(webpackEnv) {
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Provide fast-refresh https://github.com/facebook/react/tree/master/packages/react-refresh
isEnvDevelopment &&
new ReactRefreshWebpackPlugin({ disableRefreshCheck: true }),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
Expand Down
2 changes: 2 additions & 0 deletions packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"types": "./lib/react-app.d.ts",
"dependencies": {
"@babel/core": "7.9.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.3.0-beta.0",
"@svgr/webpack": "4.3.3",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
Expand Down Expand Up @@ -68,6 +69,7 @@
"postcss-safe-parser": "4.0.1",
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-refresh": "^0.8.1",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
Expand Down