Skip to content
Prev Previous commit
Next Next commit
Add feature flag env variable for react refresh
  • Loading branch information
charrondev committed Mar 31, 2020
commit c7f00320a90f387d9daea19a0f9ad1a3b9db78c2
137 changes: 137 additions & 0 deletions packages/react-dev-utils/webpackFastRefreshDevClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

// 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
// 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 url = require('url');

// Connect to WebpackDevServer via a socket.
var connection = new WebSocket(
url.format({
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
hostname: process.env.WDS_SOCKET_HOST || window.location.hostname,
port: process.env.WDS_SOCKET_PORT || window.location.port,
// Hardcoded in WebpackDevServer
pathname: process.env.WDS_SOCKET_PATH || '/sockjs-node',
slashes: true,
})
);

// Unlike WebpackDevServer client, we won't try to reconnect
// to avoid spamming the console. Disconnect usually happens
// when developer stops the server.
connection.onclose = function() {
if (typeof console !== 'undefined' && typeof console.info === 'function') {
console.info(
'The development server has disconnected.\nRefresh the page if necessary.'
);
}
};

// Remember some state related to hot module replacement.
var isFirstCompilation = true;
var mostRecentCompilationHash = null;

// Successful compilation.
function handleSuccess() {
var isHotUpdate = !isFirstCompilation;
isFirstCompilation = false;

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates();
}
}

// There is a newer version of the code available.
function handleAvailableHash(hash) {
// Update last known compilation hash.
mostRecentCompilationHash = hash;
}

// Handle messages from the server.
connection.onmessage = function(e) {
var message = JSON.parse(e.data);
switch (message.type) {
case 'hash':
handleAvailableHash(message.data);
break;
case 'still-ok':
case 'ok':
handleSuccess();
break;
case 'content-changed':
// Triggered when a file from `contentBase` changed.
window.location.reload();
break;
default:
// Do nothing.
}
};

// Is there a newer version of this code available?
function isUpdateAvailable() {
/* globals __webpack_hash__ */
// __webpack_hash__ is the hash of the current compilation.
// It's a global variable injected by webpack.
return mostRecentCompilationHash !== __webpack_hash__;
}

// webpack disallows updates in other states.
function canApplyUpdates() {
return module.hot.status() === 'idle';
}

// Attempt to update code on the fly, fall back to a hard reload.
function tryApplyUpdates(onHotUpdateSuccess) {
if (!module.hot) {
// HotModuleReplacementPlugin is not in webpack configuration.
window.location.reload();
return;
}

if (!isUpdateAvailable() || !canApplyUpdates()) {
return;
}

function handleApplyUpdates() {
if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess();
}

if (isUpdateAvailable()) {
// While we were updating, there was a new update! Do it again.
tryApplyUpdates();
}
}

// https://webpack.github.io/docs/hot-module-replacement.html#check
var result = module.hot.check(/* autoApply */ true, handleApplyUpdates);

// // webpack 2 returns a Promise instead of invoking a callback
if (result && result.then) {
result.then(
function(updatedModules) {
handleApplyUpdates(null, updatedModules);
},
function(err) {
handleApplyUpdates(err, null);
}
);
}
}
149 changes: 144 additions & 5 deletions packages/react-dev-utils/webpackHotDevClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,51 @@
// 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
// The error overlay is inspired by:
// that looks similar to our console output. 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 @@ -45,15 +82,106 @@ 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();
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();
}
}

Expand All @@ -78,6 +206,12 @@ 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 @@ -108,7 +242,12 @@ function tryApplyUpdates(onHotUpdateSuccess) {
return;
}

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

if (typeof onHotUpdateSuccess === 'function') {
// Maybe we want to do something.
onHotUpdateSuccess();
Expand Down
15 changes: 12 additions & 3 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ const appPackageJson = require(paths.appPackageJson);

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

// React refresh isn't 100% stable right now. We have a feature flag to enable it.
const shouldUseReactRefresh = process.env.REACT_REFRESH === 'true';
const webpackDevClientEntry = shouldUseReactRefresh
? require.resolve('react-dev-utils/webpackFastRefreshDevClient')
: require.resolve('react-dev-utils/webpackHotDevClient');

// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
Expand Down Expand Up @@ -161,8 +168,7 @@ module.exports = function(webpackEnv) {
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
isEnvDevelopment && webpackDevClientEntry,
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
Expand Down Expand Up @@ -420,7 +426,9 @@ module.exports = function(webpackEnv) {
},
},
},
isEnvDevelopment && require.resolve('react-refresh/babel'),
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
Expand Down Expand Up @@ -611,6 +619,7 @@ module.exports = function(webpackEnv) {
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Provide fast-refresh https://github.com/facebook/react/tree/master/packages/react-refresh
isEnvDevelopment &&
shouldUseReactRefresh &&
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.
Expand Down