Skip to content

Commit

Permalink
Make error overlay to run in the context of the iframe (facebook#3142)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tharakawj committed Oct 3, 2017
1 parent 01a0d73 commit cd3d04b
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 56 deletions.
95 changes: 95 additions & 0 deletions 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();
});
18 changes: 12 additions & 6 deletions packages/react-error-overlay/package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": [
Expand Down
57 changes: 57 additions & 0 deletions 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 <CompileErrorContainer error={currentBuildError} />;
}
if (currentRuntimeErrorRecords.length > 0) {
return (
<RuntimeErrorContainer
errorRecords={currentRuntimeErrorRecords}
close={dismissRuntimeErrors}
launchEditorEndpoint={launchEditorEndpoint}
/>
);
}
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();
87 changes: 37 additions & 50 deletions packages/react-error-overlay/src/index.js
Expand Up @@ -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 = {|
Expand All @@ -25,8 +25,8 @@ type RuntimeReportingOptions = {|

let iframe: null | HTMLIFrameElement = null;
let isLoadingIframe: boolean = false;
var isIframeReady: boolean = false;

let renderedElement: null | Element<any> = null;
let currentBuildError: null | string = null;
let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
Expand Down Expand Up @@ -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;
Expand All @@ -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 <CompileErrorContainer error={currentBuildError} />;
}
if (currentRuntimeErrorRecords.length > 0) {
if (!currentRuntimeErrorOptions) {
throw new Error('Expected options to be injected.');
}
return (
<RuntimeErrorContainer
errorRecords={currentRuntimeErrorRecords}
close={dismissRuntimeErrors}
launchEditorEndpoint={currentRuntimeErrorOptions.launchEditorEndpoint}
/>
);
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();
};
18 changes: 18 additions & 0 deletions 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');
27 changes: 27 additions & 0 deletions 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',
},
],
},
};

0 comments on commit cd3d04b

Please sign in to comment.