diff --git a/packages/react-error-overlay/build.js b/packages/react-error-overlay/build.js
new file mode 100644
index 00000000000..592da141ffe
--- /dev/null
+++ b/packages/react-error-overlay/build.js
@@ -0,0 +1,95 @@
+/**
+ * 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.
+ */
+const webpack = require('webpack');
+const chalk = require('chalk');
+const webpackConfig = require('./webpack.config.js');
+const iframeWebpackConfig = require('./webpack.config.iframe.js');
+const rimraf = require('rimraf');
+const chokidar = require('chokidar');
+
+const args = process.argv.slice(2);
+const watchMode = args[0] === '--watch' || args[0] === '-w';
+
+const isCI =
+ process.env.CI &&
+ (typeof process.env.CI !== 'string' ||
+ process.env.CI.toLowerCase() !== 'false');
+
+function build(config, name, callback) {
+ console.log(chalk.cyan('Compiling ' + name));
+ webpack(config).run((error, stats) => {
+ if (error) {
+ console.log(chalk.red('Failed to compile.'));
+ console.log(error.message || error);
+ console.log();
+ }
+
+ if (stats.compilation.errors.length) {
+ console.log(chalk.red('Failed to compile.'));
+ console.log(stats.toString({ all: false, errors: true }));
+ }
+
+ if (stats.compilation.warnings.length) {
+ console.log(chalk.yellow('Compiled with warnings.'));
+ console.log(stats.toString({ all: false, warnings: true }));
+ }
+
+ // Fail the build if running in a CI server
+ if (
+ error ||
+ stats.compilation.errors.length ||
+ stats.compilation.warnings.length
+ ) {
+ isCI && process.exit(1);
+ return;
+ }
+
+ console.log(
+ stats.toString({ colors: true, modules: false, version: false })
+ );
+ console.log();
+
+ callback(stats);
+ });
+}
+
+function runBuildSteps() {
+ build(iframeWebpackConfig, 'iframeScript.js', () => {
+ build(webpackConfig, 'index.js', () => {
+ console.log(chalk.bold.green('Compiled successfully!\n\n'));
+ });
+ });
+}
+
+function setupWatch() {
+ const watcher = chokidar.watch('./src', {
+ ignoreInitial: true,
+ });
+
+ watcher.on('change', runBuildSteps);
+ watcher.on('add', runBuildSteps);
+
+ watcher.on('ready', () => {
+ runBuildSteps();
+ });
+
+ process.on('SIGINT', function() {
+ watcher.close();
+ process.exit(0);
+ });
+
+ watcher.on('error', error => {
+ console.error('Watcher failure', error);
+ process.exit(1);
+ });
+}
+
+// Clean up lib folder
+rimraf('lib/', () => {
+ console.log('Cleaned up the lib folder.\n');
+ watchMode ? setupWatch() : runBuildSteps();
+});
diff --git a/packages/react-error-overlay/package.json b/packages/react-error-overlay/package.json
index 7640d1c5d92..82ae43ec628 100644
--- a/packages/react-error-overlay/package.json
+++ b/packages/react-error-overlay/package.json
@@ -5,10 +5,10 @@
"main": "lib/index.js",
"scripts": {
"prepublishOnly": "npm run build:prod && npm test",
- "start": "rimraf lib/ && cross-env NODE_ENV=development npm run build -- --watch",
- "test": "flow && jest",
- "build": "rimraf lib/ && babel src/ -d lib/",
- "build:prod": "rimraf lib/ && cross-env NODE_ENV=production babel src/ -d lib/"
+ "start": "cross-env NODE_ENV=development node build.js --watch",
+ "test": "flow && cross-env NODE_ENV=test jest",
+ "build": "cross-env NODE_ENV=development node build.js",
+ "build:prod": "cross-env NODE_ENV=production node build.js"
},
"repository": "facebookincubator/create-react-app",
"license": "MIT",
@@ -35,15 +35,19 @@
"babel-code-frame": "6.22.0",
"babel-runtime": "6.26.0",
"html-entities": "1.2.1",
+ "object-assign": "4.1.1",
+ "promise": "8.0.1",
"react": "^15 || ^16",
"react-dom": "^15 || ^16",
"settle-promise": "1.0.0",
"source-map": "0.5.6"
},
"devDependencies": {
- "babel-cli": "6.24.1",
"babel-eslint": "7.2.3",
"babel-preset-react-app": "^3.0.3",
+ "babel-loader": "^7.1.2",
+ "chalk": "^2.1.0",
+ "chokidar": "^1.7.0",
"cross-env": "5.0.5",
"eslint": "4.4.1",
"eslint-config-react-app": "^2.0.1",
@@ -54,7 +58,9 @@
"flow-bin": "^0.54.0",
"jest": "20.0.4",
"jest-fetch-mock": "1.2.1",
- "rimraf": "^2.6.1"
+ "raw-loader": "^0.5.1",
+ "rimraf": "^2.6.1",
+ "webpack": "^3.6.0"
},
"jest": {
"setupFiles": [
diff --git a/packages/react-error-overlay/src/iframeScript.js b/packages/react-error-overlay/src/iframeScript.js
new file mode 100644
index 00000000000..c95ea36b1a4
--- /dev/null
+++ b/packages/react-error-overlay/src/iframeScript.js
@@ -0,0 +1,57 @@
+/**
+ * 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.
+ */
+
+import './utils/pollyfills.js';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import CompileErrorContainer from './containers/CompileErrorContainer';
+import RuntimeErrorContainer from './containers/RuntimeErrorContainer';
+import { overlayStyle } from './styles';
+import { applyStyles } from './utils/dom/css';
+
+let iframeRoot = null;
+
+function render({
+ currentBuildError,
+ currentRuntimeErrorRecords,
+ dismissRuntimeErrors,
+ launchEditorEndpoint,
+}) {
+ if (currentBuildError) {
+ return ;
+ }
+ if (currentRuntimeErrorRecords.length > 0) {
+ return (
+
+ );
+ }
+ return null;
+}
+
+window.updateContent = function updateContent(errorOverlayProps) {
+ let renderedElement = render(errorOverlayProps);
+
+ if (renderedElement === null) {
+ ReactDOM.unmountComponentAtNode(iframeRoot);
+ return false;
+ }
+ // Update the overlay
+ ReactDOM.render(renderedElement, iframeRoot);
+ return true;
+};
+
+document.body.style.margin = '0';
+// Keep popup within body boundaries for iOS Safari
+document.body.style['max-width'] = '100vw';
+iframeRoot = document.createElement('div');
+applyStyles(iframeRoot, overlayStyle);
+document.body.appendChild(iframeRoot);
+window.parent.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__.iframeReady();
diff --git a/packages/react-error-overlay/src/index.js b/packages/react-error-overlay/src/index.js
index 23f9e783885..52ff9199bcb 100644
--- a/packages/react-error-overlay/src/index.js
+++ b/packages/react-error-overlay/src/index.js
@@ -6,15 +6,15 @@
*/
/* @flow */
-import React from 'react';
-import type { Element } from 'react';
-import ReactDOM from 'react-dom';
-import CompileErrorContainer from './containers/CompileErrorContainer';
-import RuntimeErrorContainer from './containers/RuntimeErrorContainer';
import { listenToRuntimeErrors } from './listenToRuntimeErrors';
-import { iframeStyle, overlayStyle } from './styles';
+import { iframeStyle } from './styles';
import { applyStyles } from './utils/dom/css';
+// Importing iframe-bundle generated in the pre build step as
+// a text using webpack raw-loader. See webpack.config.js file.
+// $FlowFixMe
+import iframeScript from 'iframeScript';
+
import type { ErrorRecord } from './listenToRuntimeErrors';
type RuntimeReportingOptions = {|
@@ -25,8 +25,8 @@ type RuntimeReportingOptions = {|
let iframe: null | HTMLIFrameElement = null;
let isLoadingIframe: boolean = false;
+var isIframeReady: boolean = false;
-let renderedElement: null | Element = null;
let currentBuildError: null | string = null;
let currentRuntimeErrorRecords: Array = [];
let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
@@ -88,15 +88,14 @@ export function stopReportingRuntimeErrors() {
}
function update() {
- renderedElement = render();
// Loading iframe can be either sync or async depending on the browser.
if (isLoadingIframe) {
// Iframe is loading.
// First render will happen soon--don't need to do anything.
return;
}
- if (iframe) {
- // Iframe has already loaded.
+ if (isIframeReady) {
+ // Iframe is ready.
// Just update it.
updateIframeContent();
return;
@@ -108,58 +107,46 @@ function update() {
loadingIframe.onload = function() {
const iframeDocument = loadingIframe.contentDocument;
if (iframeDocument != null && iframeDocument.body != null) {
- iframeDocument.body.style.margin = '0';
- // Keep popup within body boundaries for iOS Safari
- iframeDocument.body.style['max-width'] = '100vw';
- const iframeRoot = iframeDocument.createElement('div');
- applyStyles(iframeRoot, overlayStyle);
- iframeDocument.body.appendChild(iframeRoot);
-
- // Ready! Now we can update the UI.
iframe = loadingIframe;
- isLoadingIframe = false;
- updateIframeContent();
+ const script = loadingIframe.contentWindow.document.createElement(
+ 'script'
+ );
+ script.type = 'text/javascript';
+ script.innerHTML = iframeScript;
+ iframeDocument.body.appendChild(script);
}
};
const appDocument = window.document;
appDocument.body.appendChild(loadingIframe);
}
-function render() {
- if (currentBuildError) {
- return ;
- }
- if (currentRuntimeErrorRecords.length > 0) {
- if (!currentRuntimeErrorOptions) {
- throw new Error('Expected options to be injected.');
- }
- return (
-
- );
+function updateIframeContent() {
+ if (!currentRuntimeErrorOptions) {
+ throw new Error('Expected options to be injected.');
}
- return null;
-}
-function updateIframeContent() {
- if (iframe === null) {
+ if (!iframe) {
throw new Error('Iframe has not been created yet.');
}
- const iframeBody = iframe.contentDocument.body;
- if (!iframeBody) {
- throw new Error('Expected iframe to have a body.');
- }
- const iframeRoot = iframeBody.firstChild;
- if (renderedElement === null) {
- // Destroy iframe and force it to be recreated on next error
+
+ const isRendered = iframe.contentWindow.updateContent({
+ currentBuildError,
+ currentRuntimeErrorRecords,
+ dismissRuntimeErrors,
+ launchEditorEndpoint: currentRuntimeErrorOptions.launchEditorEndpoint,
+ });
+
+ if (!isRendered) {
window.document.body.removeChild(iframe);
- ReactDOM.unmountComponentAtNode(iframeRoot);
iframe = null;
- return;
+ isIframeReady = false;
}
- // Update the overlay
- ReactDOM.render(renderedElement, iframeRoot);
}
+
+window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ =
+ window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ || {};
+window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__.iframeReady = function iframeReady() {
+ isIframeReady = true;
+ isLoadingIframe = false;
+ updateIframeContent();
+};
diff --git a/packages/react-error-overlay/src/utils/pollyfills.js b/packages/react-error-overlay/src/utils/pollyfills.js
new file mode 100644
index 00000000000..ddd5aeb9651
--- /dev/null
+++ b/packages/react-error-overlay/src/utils/pollyfills.js
@@ -0,0 +1,18 @@
+/**
+ * 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.
+ */
+
+if (typeof Promise === 'undefined') {
+ // Rejection tracking prevents a common issue where React gets into an
+ // inconsistent state due to an error, but it gets swallowed by a Promise,
+ // and the user has no idea what causes React's erratic future behavior.
+ require('promise/lib/rejection-tracking').enable();
+ window.Promise = require('promise/lib/es6-extensions.js');
+}
+
+// Object.assign() is commonly used with React.
+// It will use the native implementation if it's present and isn't buggy.
+Object.assign = require('object-assign');
diff --git a/packages/react-error-overlay/webpack.config.iframe.js b/packages/react-error-overlay/webpack.config.iframe.js
new file mode 100644
index 00000000000..9fa742b720b
--- /dev/null
+++ b/packages/react-error-overlay/webpack.config.iframe.js
@@ -0,0 +1,27 @@
+/**
+ * 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';
+
+const path = require('path');
+
+module.exports = {
+ devtool: 'cheap-module-source-map',
+ entry: './src/iframeScript.js',
+ output: {
+ path: path.join(__dirname, './lib'),
+ filename: 'iframe-bundle.js',
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ include: path.resolve(__dirname, './src'),
+ use: 'babel-loader',
+ },
+ ],
+ },
+};
diff --git a/packages/react-error-overlay/webpack.config.js b/packages/react-error-overlay/webpack.config.js
new file mode 100644
index 00000000000..5d640e05ca3
--- /dev/null
+++ b/packages/react-error-overlay/webpack.config.js
@@ -0,0 +1,38 @@
+/**
+ * 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';
+
+const path = require('path');
+
+module.exports = {
+ devtool: 'cheap-module-source-map',
+ entry: './src/index.js',
+ output: {
+ path: path.join(__dirname, './lib'),
+ filename: 'index.js',
+ library: 'ReactErrorOverlay',
+ libraryTarget: 'umd',
+ },
+ module: {
+ rules: [
+ {
+ test: /iframe-bundle\.js$/,
+ use: 'raw-loader',
+ },
+ {
+ test: /\.js$/,
+ include: path.resolve(__dirname, './src'),
+ use: 'babel-loader',
+ },
+ ],
+ },
+ resolve: {
+ alias: {
+ iframeScript$: path.resolve(__dirname, './lib/iframe-bundle.js'),
+ },
+ },
+};