Skip to content

Commit cd3d04b

Browse files
authored
Make error overlay to run in the context of the iframe (#3142)
* Make error overlay to run in the context of the iframe * Configure webpack to build the entire package * Remove inline raw-loader config * Configure watch mode for error-overlay webpack build * Add polyfills to the error-overlay iframe script * Add header comment * Configure to fail CI on error or warning * Suppress flow-type error on importing iframe-bundle * Change webpack to a dev dependency and pin some versions * Disable webpack cache * Update license headers to MIT
1 parent 01a0d73 commit cd3d04b

File tree

7 files changed

+284
-56
lines changed

7 files changed

+284
-56
lines changed

packages/react-error-overlay/build.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
const webpack = require('webpack');
8+
const chalk = require('chalk');
9+
const webpackConfig = require('./webpack.config.js');
10+
const iframeWebpackConfig = require('./webpack.config.iframe.js');
11+
const rimraf = require('rimraf');
12+
const chokidar = require('chokidar');
13+
14+
const args = process.argv.slice(2);
15+
const watchMode = args[0] === '--watch' || args[0] === '-w';
16+
17+
const isCI =
18+
process.env.CI &&
19+
(typeof process.env.CI !== 'string' ||
20+
process.env.CI.toLowerCase() !== 'false');
21+
22+
function build(config, name, callback) {
23+
console.log(chalk.cyan('Compiling ' + name));
24+
webpack(config).run((error, stats) => {
25+
if (error) {
26+
console.log(chalk.red('Failed to compile.'));
27+
console.log(error.message || error);
28+
console.log();
29+
}
30+
31+
if (stats.compilation.errors.length) {
32+
console.log(chalk.red('Failed to compile.'));
33+
console.log(stats.toString({ all: false, errors: true }));
34+
}
35+
36+
if (stats.compilation.warnings.length) {
37+
console.log(chalk.yellow('Compiled with warnings.'));
38+
console.log(stats.toString({ all: false, warnings: true }));
39+
}
40+
41+
// Fail the build if running in a CI server
42+
if (
43+
error ||
44+
stats.compilation.errors.length ||
45+
stats.compilation.warnings.length
46+
) {
47+
isCI && process.exit(1);
48+
return;
49+
}
50+
51+
console.log(
52+
stats.toString({ colors: true, modules: false, version: false })
53+
);
54+
console.log();
55+
56+
callback(stats);
57+
});
58+
}
59+
60+
function runBuildSteps() {
61+
build(iframeWebpackConfig, 'iframeScript.js', () => {
62+
build(webpackConfig, 'index.js', () => {
63+
console.log(chalk.bold.green('Compiled successfully!\n\n'));
64+
});
65+
});
66+
}
67+
68+
function setupWatch() {
69+
const watcher = chokidar.watch('./src', {
70+
ignoreInitial: true,
71+
});
72+
73+
watcher.on('change', runBuildSteps);
74+
watcher.on('add', runBuildSteps);
75+
76+
watcher.on('ready', () => {
77+
runBuildSteps();
78+
});
79+
80+
process.on('SIGINT', function() {
81+
watcher.close();
82+
process.exit(0);
83+
});
84+
85+
watcher.on('error', error => {
86+
console.error('Watcher failure', error);
87+
process.exit(1);
88+
});
89+
}
90+
91+
// Clean up lib folder
92+
rimraf('lib/', () => {
93+
console.log('Cleaned up the lib folder.\n');
94+
watchMode ? setupWatch() : runBuildSteps();
95+
});

packages/react-error-overlay/package.json

+12-6
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
"main": "lib/index.js",
66
"scripts": {
77
"prepublishOnly": "npm run build:prod && npm test",
8-
"start": "rimraf lib/ && cross-env NODE_ENV=development npm run build -- --watch",
9-
"test": "flow && jest",
10-
"build": "rimraf lib/ && babel src/ -d lib/",
11-
"build:prod": "rimraf lib/ && cross-env NODE_ENV=production babel src/ -d lib/"
8+
"start": "cross-env NODE_ENV=development node build.js --watch",
9+
"test": "flow && cross-env NODE_ENV=test jest",
10+
"build": "cross-env NODE_ENV=development node build.js",
11+
"build:prod": "cross-env NODE_ENV=production node build.js"
1212
},
1313
"repository": "facebookincubator/create-react-app",
1414
"license": "MIT",
@@ -35,15 +35,19 @@
3535
"babel-code-frame": "6.22.0",
3636
"babel-runtime": "6.26.0",
3737
"html-entities": "1.2.1",
38+
"object-assign": "4.1.1",
39+
"promise": "8.0.1",
3840
"react": "^15 || ^16",
3941
"react-dom": "^15 || ^16",
4042
"settle-promise": "1.0.0",
4143
"source-map": "0.5.6"
4244
},
4345
"devDependencies": {
44-
"babel-cli": "6.24.1",
4546
"babel-eslint": "7.2.3",
4647
"babel-preset-react-app": "^3.0.3",
48+
"babel-loader": "^7.1.2",
49+
"chalk": "^2.1.0",
50+
"chokidar": "^1.7.0",
4751
"cross-env": "5.0.5",
4852
"eslint": "4.4.1",
4953
"eslint-config-react-app": "^2.0.1",
@@ -54,7 +58,9 @@
5458
"flow-bin": "^0.54.0",
5559
"jest": "20.0.4",
5660
"jest-fetch-mock": "1.2.1",
57-
"rimraf": "^2.6.1"
61+
"raw-loader": "^0.5.1",
62+
"rimraf": "^2.6.1",
63+
"webpack": "^3.6.0"
5864
},
5965
"jest": {
6066
"setupFiles": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
import './utils/pollyfills.js';
9+
import React from 'react';
10+
import ReactDOM from 'react-dom';
11+
import CompileErrorContainer from './containers/CompileErrorContainer';
12+
import RuntimeErrorContainer from './containers/RuntimeErrorContainer';
13+
import { overlayStyle } from './styles';
14+
import { applyStyles } from './utils/dom/css';
15+
16+
let iframeRoot = null;
17+
18+
function render({
19+
currentBuildError,
20+
currentRuntimeErrorRecords,
21+
dismissRuntimeErrors,
22+
launchEditorEndpoint,
23+
}) {
24+
if (currentBuildError) {
25+
return <CompileErrorContainer error={currentBuildError} />;
26+
}
27+
if (currentRuntimeErrorRecords.length > 0) {
28+
return (
29+
<RuntimeErrorContainer
30+
errorRecords={currentRuntimeErrorRecords}
31+
close={dismissRuntimeErrors}
32+
launchEditorEndpoint={launchEditorEndpoint}
33+
/>
34+
);
35+
}
36+
return null;
37+
}
38+
39+
window.updateContent = function updateContent(errorOverlayProps) {
40+
let renderedElement = render(errorOverlayProps);
41+
42+
if (renderedElement === null) {
43+
ReactDOM.unmountComponentAtNode(iframeRoot);
44+
return false;
45+
}
46+
// Update the overlay
47+
ReactDOM.render(renderedElement, iframeRoot);
48+
return true;
49+
};
50+
51+
document.body.style.margin = '0';
52+
// Keep popup within body boundaries for iOS Safari
53+
document.body.style['max-width'] = '100vw';
54+
iframeRoot = document.createElement('div');
55+
applyStyles(iframeRoot, overlayStyle);
56+
document.body.appendChild(iframeRoot);
57+
window.parent.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__.iframeReady();

packages/react-error-overlay/src/index.js

+37-50
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
*/
77

88
/* @flow */
9-
import React from 'react';
10-
import type { Element } from 'react';
11-
import ReactDOM from 'react-dom';
12-
import CompileErrorContainer from './containers/CompileErrorContainer';
13-
import RuntimeErrorContainer from './containers/RuntimeErrorContainer';
149
import { listenToRuntimeErrors } from './listenToRuntimeErrors';
15-
import { iframeStyle, overlayStyle } from './styles';
10+
import { iframeStyle } from './styles';
1611
import { applyStyles } from './utils/dom/css';
1712

13+
// Importing iframe-bundle generated in the pre build step as
14+
// a text using webpack raw-loader. See webpack.config.js file.
15+
// $FlowFixMe
16+
import iframeScript from 'iframeScript';
17+
1818
import type { ErrorRecord } from './listenToRuntimeErrors';
1919

2020
type RuntimeReportingOptions = {|
@@ -25,8 +25,8 @@ type RuntimeReportingOptions = {|
2525

2626
let iframe: null | HTMLIFrameElement = null;
2727
let isLoadingIframe: boolean = false;
28+
var isIframeReady: boolean = false;
2829

29-
let renderedElement: null | Element<any> = null;
3030
let currentBuildError: null | string = null;
3131
let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
3232
let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
@@ -88,15 +88,14 @@ export function stopReportingRuntimeErrors() {
8888
}
8989

9090
function update() {
91-
renderedElement = render();
9291
// Loading iframe can be either sync or async depending on the browser.
9392
if (isLoadingIframe) {
9493
// Iframe is loading.
9594
// First render will happen soon--don't need to do anything.
9695
return;
9796
}
98-
if (iframe) {
99-
// Iframe has already loaded.
97+
if (isIframeReady) {
98+
// Iframe is ready.
10099
// Just update it.
101100
updateIframeContent();
102101
return;
@@ -108,58 +107,46 @@ function update() {
108107
loadingIframe.onload = function() {
109108
const iframeDocument = loadingIframe.contentDocument;
110109
if (iframeDocument != null && iframeDocument.body != null) {
111-
iframeDocument.body.style.margin = '0';
112-
// Keep popup within body boundaries for iOS Safari
113-
iframeDocument.body.style['max-width'] = '100vw';
114-
const iframeRoot = iframeDocument.createElement('div');
115-
applyStyles(iframeRoot, overlayStyle);
116-
iframeDocument.body.appendChild(iframeRoot);
117-
118-
// Ready! Now we can update the UI.
119110
iframe = loadingIframe;
120-
isLoadingIframe = false;
121-
updateIframeContent();
111+
const script = loadingIframe.contentWindow.document.createElement(
112+
'script'
113+
);
114+
script.type = 'text/javascript';
115+
script.innerHTML = iframeScript;
116+
iframeDocument.body.appendChild(script);
122117
}
123118
};
124119
const appDocument = window.document;
125120
appDocument.body.appendChild(loadingIframe);
126121
}
127122

128-
function render() {
129-
if (currentBuildError) {
130-
return <CompileErrorContainer error={currentBuildError} />;
131-
}
132-
if (currentRuntimeErrorRecords.length > 0) {
133-
if (!currentRuntimeErrorOptions) {
134-
throw new Error('Expected options to be injected.');
135-
}
136-
return (
137-
<RuntimeErrorContainer
138-
errorRecords={currentRuntimeErrorRecords}
139-
close={dismissRuntimeErrors}
140-
launchEditorEndpoint={currentRuntimeErrorOptions.launchEditorEndpoint}
141-
/>
142-
);
123+
function updateIframeContent() {
124+
if (!currentRuntimeErrorOptions) {
125+
throw new Error('Expected options to be injected.');
143126
}
144-
return null;
145-
}
146127

147-
function updateIframeContent() {
148-
if (iframe === null) {
128+
if (!iframe) {
149129
throw new Error('Iframe has not been created yet.');
150130
}
151-
const iframeBody = iframe.contentDocument.body;
152-
if (!iframeBody) {
153-
throw new Error('Expected iframe to have a body.');
154-
}
155-
const iframeRoot = iframeBody.firstChild;
156-
if (renderedElement === null) {
157-
// Destroy iframe and force it to be recreated on next error
131+
132+
const isRendered = iframe.contentWindow.updateContent({
133+
currentBuildError,
134+
currentRuntimeErrorRecords,
135+
dismissRuntimeErrors,
136+
launchEditorEndpoint: currentRuntimeErrorOptions.launchEditorEndpoint,
137+
});
138+
139+
if (!isRendered) {
158140
window.document.body.removeChild(iframe);
159-
ReactDOM.unmountComponentAtNode(iframeRoot);
160141
iframe = null;
161-
return;
142+
isIframeReady = false;
162143
}
163-
// Update the overlay
164-
ReactDOM.render(renderedElement, iframeRoot);
165144
}
145+
146+
window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ =
147+
window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ || {};
148+
window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__.iframeReady = function iframeReady() {
149+
isIframeReady = true;
150+
isLoadingIframe = false;
151+
updateIframeContent();
152+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
if (typeof Promise === 'undefined') {
9+
// Rejection tracking prevents a common issue where React gets into an
10+
// inconsistent state due to an error, but it gets swallowed by a Promise,
11+
// and the user has no idea what causes React's erratic future behavior.
12+
require('promise/lib/rejection-tracking').enable();
13+
window.Promise = require('promise/lib/es6-extensions.js');
14+
}
15+
16+
// Object.assign() is commonly used with React.
17+
// It will use the native implementation if it's present and isn't buggy.
18+
Object.assign = require('object-assign');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
'use strict';
8+
9+
const path = require('path');
10+
11+
module.exports = {
12+
devtool: 'cheap-module-source-map',
13+
entry: './src/iframeScript.js',
14+
output: {
15+
path: path.join(__dirname, './lib'),
16+
filename: 'iframe-bundle.js',
17+
},
18+
module: {
19+
rules: [
20+
{
21+
test: /\.js$/,
22+
include: path.resolve(__dirname, './src'),
23+
use: 'babel-loader',
24+
},
25+
],
26+
},
27+
};

0 commit comments

Comments
 (0)