Skip to content

Commit c7f0032

Browse files
committed
Add feature flag env variable for react refresh
1 parent 78065e6 commit c7f0032

File tree

3 files changed

+293
-8
lines changed

3 files changed

+293
-8
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
// This alternative WebpackDevServer combines the functionality of:
11+
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
12+
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
13+
// It only supports their simplest configuration (hot updates on same server).
14+
// It makes some opinionated choices on top, like adding a syntax error overlay
15+
// The error overlay is inspired by:
16+
// https://github.com/glenjamin/webpack-hot-middleware
17+
// The error overlay is provided by:
18+
// https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/src/overlay
19+
20+
var url = require('url');
21+
22+
// Connect to WebpackDevServer via a socket.
23+
var connection = new WebSocket(
24+
url.format({
25+
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
26+
hostname: process.env.WDS_SOCKET_HOST || window.location.hostname,
27+
port: process.env.WDS_SOCKET_PORT || window.location.port,
28+
// Hardcoded in WebpackDevServer
29+
pathname: process.env.WDS_SOCKET_PATH || '/sockjs-node',
30+
slashes: true,
31+
})
32+
);
33+
34+
// Unlike WebpackDevServer client, we won't try to reconnect
35+
// to avoid spamming the console. Disconnect usually happens
36+
// when developer stops the server.
37+
connection.onclose = function() {
38+
if (typeof console !== 'undefined' && typeof console.info === 'function') {
39+
console.info(
40+
'The development server has disconnected.\nRefresh the page if necessary.'
41+
);
42+
}
43+
};
44+
45+
// Remember some state related to hot module replacement.
46+
var isFirstCompilation = true;
47+
var mostRecentCompilationHash = null;
48+
49+
// Successful compilation.
50+
function handleSuccess() {
51+
var isHotUpdate = !isFirstCompilation;
52+
isFirstCompilation = false;
53+
54+
// Attempt to apply hot updates or reload.
55+
if (isHotUpdate) {
56+
tryApplyUpdates();
57+
}
58+
}
59+
60+
// There is a newer version of the code available.
61+
function handleAvailableHash(hash) {
62+
// Update last known compilation hash.
63+
mostRecentCompilationHash = hash;
64+
}
65+
66+
// Handle messages from the server.
67+
connection.onmessage = function(e) {
68+
var message = JSON.parse(e.data);
69+
switch (message.type) {
70+
case 'hash':
71+
handleAvailableHash(message.data);
72+
break;
73+
case 'still-ok':
74+
case 'ok':
75+
handleSuccess();
76+
break;
77+
case 'content-changed':
78+
// Triggered when a file from `contentBase` changed.
79+
window.location.reload();
80+
break;
81+
default:
82+
// Do nothing.
83+
}
84+
};
85+
86+
// Is there a newer version of this code available?
87+
function isUpdateAvailable() {
88+
/* globals __webpack_hash__ */
89+
// __webpack_hash__ is the hash of the current compilation.
90+
// It's a global variable injected by webpack.
91+
return mostRecentCompilationHash !== __webpack_hash__;
92+
}
93+
94+
// webpack disallows updates in other states.
95+
function canApplyUpdates() {
96+
return module.hot.status() === 'idle';
97+
}
98+
99+
// Attempt to update code on the fly, fall back to a hard reload.
100+
function tryApplyUpdates(onHotUpdateSuccess) {
101+
if (!module.hot) {
102+
// HotModuleReplacementPlugin is not in webpack configuration.
103+
window.location.reload();
104+
return;
105+
}
106+
107+
if (!isUpdateAvailable() || !canApplyUpdates()) {
108+
return;
109+
}
110+
111+
function handleApplyUpdates() {
112+
if (typeof onHotUpdateSuccess === 'function') {
113+
// Maybe we want to do something.
114+
onHotUpdateSuccess();
115+
}
116+
117+
if (isUpdateAvailable()) {
118+
// While we were updating, there was a new update! Do it again.
119+
tryApplyUpdates();
120+
}
121+
}
122+
123+
// https://webpack.github.io/docs/hot-module-replacement.html#check
124+
var result = module.hot.check(/* autoApply */ true, handleApplyUpdates);
125+
126+
// // webpack 2 returns a Promise instead of invoking a callback
127+
if (result && result.then) {
128+
result.then(
129+
function(updatedModules) {
130+
handleApplyUpdates(null, updatedModules);
131+
},
132+
function(err) {
133+
handleApplyUpdates(err, null);
134+
}
135+
);
136+
}
137+
}

packages/react-dev-utils/webpackHotDevClient.js

Lines changed: 144 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,51 @@
1010
// This alternative WebpackDevServer combines the functionality of:
1111
// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js
1212
// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js
13+
1314
// It only supports their simplest configuration (hot updates on same server).
1415
// It makes some opinionated choices on top, like adding a syntax error overlay
15-
// The error overlay is inspired by:
16+
// that looks similar to our console output. The error overlay is inspired by:
1617
// https://github.com/glenjamin/webpack-hot-middleware
17-
// The error overlay is provided by:
18-
// https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/src/overlay
1918

19+
var stripAnsi = require('strip-ansi');
2020
var url = require('url');
21+
var launchEditorEndpoint = require('./launchEditorEndpoint');
22+
var formatWebpackMessages = require('./formatWebpackMessages');
23+
var ErrorOverlay = require('react-error-overlay');
24+
25+
ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) {
26+
// Keep this sync with errorOverlayMiddleware.js
27+
fetch(
28+
launchEditorEndpoint +
29+
'?fileName=' +
30+
window.encodeURIComponent(errorLocation.fileName) +
31+
'&lineNumber=' +
32+
window.encodeURIComponent(errorLocation.lineNumber || 1) +
33+
'&colNumber=' +
34+
window.encodeURIComponent(errorLocation.colNumber || 1)
35+
);
36+
});
37+
38+
// We need to keep track of if there has been a runtime error.
39+
// Essentially, we cannot guarantee application state was not corrupted by the
40+
// runtime error. To prevent confusing behavior, we forcibly reload the entire
41+
// application. This is handled below when we are notified of a compile (code
42+
// change).
43+
// See https://github.com/facebook/create-react-app/issues/3096
44+
var hadRuntimeError = false;
45+
ErrorOverlay.startReportingRuntimeErrors({
46+
onError: function() {
47+
hadRuntimeError = true;
48+
},
49+
filename: '/static/js/bundle.js',
50+
});
51+
52+
if (module.hot && typeof module.hot.dispose === 'function') {
53+
module.hot.dispose(function() {
54+
// TODO: why do we need this?
55+
ErrorOverlay.stopReportingRuntimeErrors();
56+
});
57+
}
2158

2259
// Connect to WebpackDevServer via a socket.
2360
var connection = new WebSocket(
@@ -45,15 +82,106 @@ connection.onclose = function() {
4582
// Remember some state related to hot module replacement.
4683
var isFirstCompilation = true;
4784
var mostRecentCompilationHash = null;
85+
var hasCompileErrors = false;
86+
87+
function clearOutdatedErrors() {
88+
// Clean up outdated compile errors, if any.
89+
if (typeof console !== 'undefined' && typeof console.clear === 'function') {
90+
if (hasCompileErrors) {
91+
console.clear();
92+
}
93+
}
94+
}
4895

4996
// Successful compilation.
5097
function handleSuccess() {
98+
clearOutdatedErrors();
99+
51100
var isHotUpdate = !isFirstCompilation;
52101
isFirstCompilation = false;
102+
hasCompileErrors = false;
53103

54104
// Attempt to apply hot updates or reload.
55105
if (isHotUpdate) {
56-
tryApplyUpdates();
106+
tryApplyUpdates(function onHotUpdateSuccess() {
107+
// Only dismiss it when we're sure it's a hot update.
108+
// Otherwise it would flicker right before the reload.
109+
tryDismissErrorOverlay();
110+
});
111+
}
112+
}
113+
114+
// Compilation with warnings (e.g. ESLint).
115+
function handleWarnings(warnings) {
116+
clearOutdatedErrors();
117+
118+
var isHotUpdate = !isFirstCompilation;
119+
isFirstCompilation = false;
120+
hasCompileErrors = false;
121+
122+
function printWarnings() {
123+
// Print warnings to the console.
124+
var formatted = formatWebpackMessages({
125+
warnings: warnings,
126+
errors: [],
127+
});
128+
129+
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
130+
for (var i = 0; i < formatted.warnings.length; i++) {
131+
if (i === 5) {
132+
console.warn(
133+
'There were more warnings in other files.\n' +
134+
'You can find a complete log in the terminal.'
135+
);
136+
break;
137+
}
138+
console.warn(stripAnsi(formatted.warnings[i]));
139+
}
140+
}
141+
}
142+
143+
printWarnings();
144+
145+
// Attempt to apply hot updates or reload.
146+
if (isHotUpdate) {
147+
tryApplyUpdates(function onSuccessfulHotUpdate() {
148+
// Only dismiss it when we're sure it's a hot update.
149+
// Otherwise it would flicker right before the reload.
150+
tryDismissErrorOverlay();
151+
});
152+
}
153+
}
154+
155+
// Compilation with errors (e.g. syntax error or missing modules).
156+
function handleErrors(errors) {
157+
clearOutdatedErrors();
158+
159+
isFirstCompilation = false;
160+
hasCompileErrors = true;
161+
162+
// "Massage" webpack messages.
163+
var formatted = formatWebpackMessages({
164+
errors: errors,
165+
warnings: [],
166+
});
167+
168+
// Only show the first error.
169+
ErrorOverlay.reportBuildError(formatted.errors[0]);
170+
171+
// Also log them to the console.
172+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
173+
for (var i = 0; i < formatted.errors.length; i++) {
174+
console.error(stripAnsi(formatted.errors[i]));
175+
}
176+
}
177+
178+
// Do not attempt to reload now.
179+
// We will reload on next success instead.
180+
}
181+
182+
function tryDismissErrorOverlay() {
183+
if (!hasCompileErrors) {
184+
ErrorOverlay.dismissBuildError();
57185
}
58186
}
59187

@@ -78,6 +206,12 @@ connection.onmessage = function(e) {
78206
// Triggered when a file from `contentBase` changed.
79207
window.location.reload();
80208
break;
209+
case 'warnings':
210+
handleWarnings(message.data);
211+
break;
212+
case 'errors':
213+
handleErrors(message.data);
214+
break;
81215
default:
82216
// Do nothing.
83217
}
@@ -108,7 +242,12 @@ function tryApplyUpdates(onHotUpdateSuccess) {
108242
return;
109243
}
110244

111-
function handleApplyUpdates() {
245+
function handleApplyUpdates(err, updatedModules) {
246+
if (err || !updatedModules || hadRuntimeError) {
247+
window.location.reload();
248+
return;
249+
}
250+
112251
if (typeof onHotUpdateSuccess === 'function') {
113252
// Maybe we want to do something.
114253
onHotUpdateSuccess();

packages/react-scripts/config/webpack.config.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ const appPackageJson = require(paths.appPackageJson);
4242

4343
// Source maps are resource heavy and can cause out of memory issue for large source files.
4444
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
45+
46+
// React refresh isn't 100% stable right now. We have a feature flag to enable it.
47+
const shouldUseReactRefresh = process.env.REACT_REFRESH === 'true';
48+
const webpackDevClientEntry = shouldUseReactRefresh
49+
? require.resolve('react-dev-utils/webpackFastRefreshDevClient')
50+
: require.resolve('react-dev-utils/webpackHotDevClient');
51+
4552
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
4653
// makes for a smoother build process.
4754
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
@@ -161,8 +168,7 @@ module.exports = function(webpackEnv) {
161168
// the line below with these two lines if you prefer the stock client:
162169
// require.resolve('webpack-dev-server/client') + '?/',
163170
// require.resolve('webpack/hot/dev-server'),
164-
isEnvDevelopment &&
165-
require.resolve('react-dev-utils/webpackHotDevClient'),
171+
isEnvDevelopment && webpackDevClientEntry,
166172
// Finally, this is your app's code:
167173
paths.appIndexJs,
168174
// We include the app code last so that if there is a runtime error during
@@ -420,7 +426,9 @@ module.exports = function(webpackEnv) {
420426
},
421427
},
422428
},
423-
isEnvDevelopment && require.resolve('react-refresh/babel'),
429+
isEnvDevelopment &&
430+
shouldUseReactRefresh &&
431+
require.resolve('react-refresh/babel'),
424432
].filter(Boolean),
425433
],
426434
// This is a feature of `babel-loader` for webpack (not Babel itself).
@@ -611,6 +619,7 @@ module.exports = function(webpackEnv) {
611619
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
612620
// Provide fast-refresh https://github.com/facebook/react/tree/master/packages/react-refresh
613621
isEnvDevelopment &&
622+
shouldUseReactRefresh &&
614623
new ReactRefreshWebpackPlugin({ disableRefreshCheck: true }),
615624
// Watcher doesn't work well if you mistype casing in a path so we use
616625
// a plugin that prints an error when you attempt to do this.

0 commit comments

Comments
 (0)