diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md
index 4a07dbae28e..a36af204412 100644
--- a/packages/react-dev-utils/README.md
+++ b/packages/react-dev-utils/README.md
@@ -178,3 +178,25 @@ prompt(
}
});
```
+
+#### `webpackHotDevClient.js`
+
+This is an alternative client for [WebpackDevServer](https://github.com/webpack/webpack-dev-server) that shows a syntax error overlay.
+
+It currently supports only Webpack 1.x.
+
+```js
+// Webpack development config
+module.exports = {
+ // ...
+ entry: [
+ // You can replace 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'),
+ 'react-dev-utils/webpackHotDevClient',
+ 'src/index'
+ ],
+ // ...
+}
+```
diff --git a/packages/react-dev-utils/formatWebpackMessages.js b/packages/react-dev-utils/formatWebpackMessages.js
index 4fda66d4b39..d72d5f734ca 100644
--- a/packages/react-dev-utils/formatWebpackMessages.js
+++ b/packages/react-dev-utils/formatWebpackMessages.js
@@ -7,50 +7,115 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
+// WARNING: this code is untranspiled and is used in browser too.
+// Please make sure any changes are in ES5 or contribute a Babel compile step.
+
// Some custom utilities to prettify Webpack output.
-// This is a little hacky.
-// It would be easier if webpack provided a rich error object.
+// This is quite hacky and hopefully won't be needed when Webpack fixes this.
+// https://github.com/webpack/webpack/issues/2878
+
var friendlySyntaxErrorLabel = 'Syntax error:';
+
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
+
+// Cleans up webpack error messages.
function formatMessage(message) {
- return message
- // Make some common errors shorter:
- .replace(
- // Babel syntax error
+ var lines = message.split('\n');
+
+ // line #0 is filename
+ // line #1 is the main error message
+ if (!lines[0] || !lines[1]) {
+ return message;
+ }
+
+ // Remove webpack-specific loader notation from filename.
+ // Before:
+ // ./~/css-loader!./~/postcss-loader!./src/App.css
+ // After:
+ // ./src/App.css
+ if (lines[0].lastIndexOf('!') !== -1) {
+ lines[0] = lines[0].substr(lines[0].lastIndexOf('!') + 1);
+ }
+
+ // Cleans up verbose "module not found" messages for files and packages.
+ if (lines[1].indexOf('Module not found: ') === 0) {
+ lines = [
+ lines[0],
+ // Clean up message because "Module not found: " is descriptive enough.
+ lines[1].replace(
+ 'Cannot resolve \'file\' or \'directory\' ', ''
+ ).replace(
+ 'Cannot resolve module ', ''
+ ).replace(
+ 'Error: ', ''
+ ),
+ // Skip all irrelevant lines.
+ // (For some reason they only appear on the client in browser.)
+ '',
+ lines[lines.length - 1] // error location is the last line
+ ]
+ }
+
+ // Cleans up syntax error messages.
+ if (lines[1].indexOf('Module build failed: ') === 0) {
+ // For some reason, on the client messages appear duplicated:
+ // https://github.com/webpack/webpack/issues/3008
+ // This won't happen in Node but since we share this helpers,
+ // we will dedupe them right here. We will ignore all lines
+ // after the original error message text is repeated the second time.
+ var errorText = lines[1].substr('Module build failed: '.length);
+ var cleanedLines = [];
+ var hasReachedDuplicateMessage = false;
+ // Gather lines until we reach the beginning of duplicate message.
+ lines.forEach(function(line, index) {
+ if (
+ // First time it occurs is fine.
+ index !== 1 &&
+ // line.endsWith(errorText)
+ line.length >= errorText.length &&
+ line.indexOf(errorText) === line.length - errorText.length
+ ) {
+ // We see the same error message for the second time!
+ // Filter out repeated error message and everything after it.
+ hasReachedDuplicateMessage = true;
+ }
+ if (
+ !hasReachedDuplicateMessage ||
+ // Print last line anyway because it contains the source location
+ index === lines.length - 1
+ ) {
+ // This line is OK to appear in the output.
+ cleanedLines.push(line);
+ }
+ });
+ // We are clean now!
+ lines = cleanedLines;
+ // Finally, brush up the error message a little.
+ lines[1] = lines[1].replace(
'Module build failed: SyntaxError:',
friendlySyntaxErrorLabel
- )
- .replace(
- // Webpack file not found error
- /Module not found: Error: Cannot resolve 'file' or 'directory'/,
- 'Module not found:'
- )
- // Internal stacks are generally useless so we strip them
- .replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y
- // Webpack loader names obscure CSS filenames
- .replace('./~/css-loader!./~/postcss-loader!', '');
+ );
+ }
+
+ // Reassemble the message.
+ message = lines.join('\n');
+ // Internal stacks are generally useless so we strip them
+ message = message.replace(
+ /^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, ''
+ ); // at ... ...:x:y
+
+ return message;
}
-function formatWebpackMessages(stats) {
- var hasErrors = stats.hasErrors();
- var hasWarnings = stats.hasWarnings();
- if (!hasErrors && !hasWarnings) {
- return {
- errors: [],
- warnings: []
- };
- }
- // We use stats.toJson({}, true) to make output more compact and readable:
- // https://github.com/facebookincubator/create-react-app/issues/401#issuecomment-238291901
- var json = stats.toJson({}, true);
- var formattedErrors = json.errors.map(message =>
- 'Error in ' + formatMessage(message)
- );
- var formattedWarnings = json.warnings.map(message =>
- 'Warning in ' + formatMessage(message)
- );
+function formatWebpackMessages(json) {
+ var formattedErrors = json.errors.map(function(message) {
+ return 'Error in ' + formatMessage(message)
+ });
+ var formattedWarnings = json.warnings.map(function(message) {
+ return 'Warning in ' + formatMessage(message)
+ });
var result = {
errors: formattedErrors,
warnings: formattedWarnings
diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json
index b2fad26cefd..69439a11311 100644
--- a/packages/react-dev-utils/package.json
+++ b/packages/react-dev-utils/package.json
@@ -18,12 +18,17 @@
"openChrome.applescript",
"openBrowser.js",
"prompt.js",
- "WatchMissingNodeModulesPlugin.js"
+ "WatchMissingNodeModulesPlugin.js",
+ "webpackHotDevClient.js"
],
"dependencies": {
+ "ansi-html": "0.0.5",
"chalk": "1.1.3",
"escape-string-regexp": "1.0.5",
- "opn": "4.0.2"
+ "html-entities": "1.2.0",
+ "opn": "4.0.2",
+ "sockjs-client": "1.0.3",
+ "strip-ansi": "3.0.1"
},
"peerDependencies": {
"webpack": "^1.13.2"
diff --git a/packages/react-dev-utils/webpackHotDevClient.js b/packages/react-dev-utils/webpackHotDevClient.js
new file mode 100644
index 00000000000..d0aa54adb10
--- /dev/null
+++ b/packages/react-dev-utils/webpackHotDevClient.js
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+// 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:
+// https://github.com/glenjamin/webpack-hot-middleware
+
+var ansiHTML = require('ansi-html');
+var SockJS = require('sockjs-client');
+var stripAnsi = require('strip-ansi');
+var url = require('url');
+var formatWebpackMessages = require('./formatWebpackMessages');
+var Entities = require('html-entities').AllHtmlEntities;
+var entities = new Entities();
+
+// Color scheme inspired by https://github.com/glenjamin/webpack-hot-middleware
+var colors = {
+ reset: ['transparent', 'transparent'],
+ black: '181818',
+ red: 'E36049',
+ green: 'B3CB74',
+ yellow: 'FFD080',
+ blue: '7CAFC2',
+ magenta: '7FACCA',
+ cyan: 'C3C2EF',
+ lightgrey: 'EBE7E3',
+ darkgrey: '6D7891'
+};
+ansiHTML.setColors(colors);
+
+function showErrorOverlay(message) {
+ // Use an iframe so that document styles don't mess up the overlay.
+ var iframeID = 'react-dev-utils-webpack-hot-dev-client-overlay';
+ var iframe =
+ document.getElementById(iframeID) ||
+ document.createElement('iframe');
+ iframe.id = iframeID;
+ iframe.style.position = 'fixed';
+ iframe.style.left = 0;
+ iframe.style.top = 0;
+ iframe.style.right = 0;
+ iframe.style.bottom = 0;
+ iframe.style.width = '100vw';
+ iframe.style.height = '100vh';
+ iframe.style.border = 'none';
+ iframe.style.zIndex = 9999999999;
+ document.body.appendChild(iframe);
+
+ // Inside, make a div.
+ var overlayID = 'react-dev-utils-webpack-hot-dev-client-overlay-div';
+ var overlay =
+ iframe.contentDocument.getElementById(overlayID) ||
+ iframe.contentDocument.createElement('div');
+ overlay.id = overlayID;
+ overlay.style.position = 'fixed';
+ overlay.style.left = 0;
+ overlay.style.top = 0;
+ overlay.style.right = 0;
+ overlay.style.bottom = 0;
+ overlay.style.width = '100vw';
+ overlay.style.height = '100vh';
+ overlay.style.backgroundColor = 'black';
+ overlay.style.color = '#E8E8E8';
+ overlay.style.fontFamily = 'Menlo, Consolas, monospace';
+ overlay.style.fontSize = 'large';
+ overlay.style.padding = '2rem';
+ overlay.style.lineHeight = '1.2';
+ overlay.style.whiteSpace = 'pre-wrap';
+ overlay.style.overflow = 'auto';
+
+ // Make it look similar to our terminal.
+ overlay.innerHTML =
+ 'Failed to compile.
' +
+ ansiHTML(entities.encode(message));
+
+ // Render!
+ iframe.contentDocument.body.appendChild(overlay);
+}
+
+// Connect to WebpackDevServer via a socket.
+var connection = new SockJS(url.format({
+ protocol: window.location.protocol,
+ hostname: window.location.hostname,
+ port: window.location.port,
+ // Hardcoded in WebpackDevServer
+ pathname: '/sockjs-node'
+}));
+// Note: unlike WebpackDevServer's built-in client,
+// we don't handle disconnect. If connection fails,
+// just leave it instead of spamming the console.
+
+// 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();
+ }
+}
+
+// Compilation with warnings (e.g. ESLint).
+function handleWarnings(warnings) {
+ var isHotUpdate = !isFirstCompilation;
+ isFirstCompilation = false;
+
+ function printWarnings() {
+ // Print warnings to the console.
+ for (var i = 0; i < warnings.length; i++) {
+ console.warn(stripAnsi(warnings[i]));
+ }
+ }
+
+ // Attempt to apply hot updates or reload.
+ if (isHotUpdate) {
+ tryApplyUpdates(function onSuccessfulHotUpdate() {
+ // Only print warnings if we aren't refreshing the page.
+ // Otherwise they'll disappear right away anyway.
+ printWarnings();
+ });
+ } else {
+ // Print initial warnings immediately.
+ printWarnings();
+ }
+}
+
+// Compilation with errors (e.g. syntax error or missing modules).
+function handleErrors(errors) {
+ isFirstCompilation = false;
+
+ // "Massage" webpack messages.
+ var formatted = formatWebpackMessages({
+ errors: errors,
+ warnings: []
+ });
+
+ // Only show the first error.
+ showErrorOverlay(formatted.errors[0]);
+ // Do not attempt to reload now.
+ // We will reload on next success instead.
+}
+
+// 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 'ok':
+ handleSuccess();
+ break;
+ case 'warnings':
+ handleWarnings(message.data);
+ break;
+ case 'errors':
+ handleErrors(message.data);
+ 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;
+ }
+
+ // https://webpack.github.io/docs/hot-module-replacement.html#check
+ module.hot.check(/* autoApply */true, function(err, updatedModules) {
+ if (err || !updatedModules) {
+ window.location.reload();
+ return;
+ }
+
+ 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();
+ }
+ });
+};
diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js
index 0c3e336e781..67ba6f122a3 100644
--- a/packages/react-scripts/config/webpack.config.dev.js
+++ b/packages/react-scripts/config/webpack.config.dev.js
@@ -43,22 +43,18 @@ module.exports = {
// This means they will be the "root" imports that are included in JS bundle.
// The first two entry points enable "hot" CSS and auto-refreshes for JS.
entry: [
- // Include WebpackDevServer client. It connects to WebpackDevServer via
- // sockets and waits for recompile notifications. When WebpackDevServer
- // recompiles, it sends a message to the client by socket. If only CSS
- // was changed, the app reload just the CSS. Otherwise, it will refresh.
- // The "?/" bit at the end tells the client to look for the socket at
- // the root path, i.e. /sockjs-node/. Otherwise visiting a client-side
- // route like /todos/42 would make it wrongly request /todos/42/sockjs-node.
- // The socket server is a part of WebpackDevServer which we are using.
- // The /sockjs-node/ path I'm referring to is hardcoded in WebpackDevServer.
- require.resolve('webpack-dev-server/client') + '?/',
- // Include Webpack hot module replacement runtime. Webpack is pretty
- // low-level so we need to put all the pieces together. The runtime listens
- // to the events received by the client above, and applies updates (such as
- // new CSS) to the running application.
- require.resolve('webpack/hot/dev-server'),
- // We ship a few polyfills by default.
+ // Include an alternative client for WebpackDevServer. A client's job is to
+ // connect to WebpackDevServer by a socket and get notified about changes.
+ // When you save a file, the client will either apply hot updates (in case
+ // of CSS changes), or refresh the page (in case of JS changes). When you
+ // make a syntax error, this client will display a syntax error overlay.
+ // Note: instead of the default WebpackDevServer client, we use a custom one
+ // to bring better experience for Create React App users. You can replace
+ // 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'),
+ require.resolve('react-dev-utils/webpackHotDevClient'),
+ // We ship a few polyfills by default:
require.resolve('./polyfills'),
// Finally, this is your app's code:
paths.appIndexJs
diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js
index b47e61cfdcd..297507f18c6 100644
--- a/packages/react-scripts/scripts/start.js
+++ b/packages/react-scripts/scripts/start.js
@@ -76,7 +76,7 @@ function setupCompiler(host, port, protocol) {
// We have switched off the default Webpack output in WebpackDevServer
// options so we are going to "massage" the warnings and errors and present
// them in a readable focused way.
- var messages = formatWebpackMessages(stats);
+ var messages = formatWebpackMessages(stats.toJson({}, true));
if (!messages.errors.length && !messages.warnings.length) {
console.log(chalk.green('Compiled successfully!'));
console.log();