Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make error overlay to run in the context of the iframe #3142

Merged
merged 11 commits into from
Oct 3, 2017
95 changes: 95 additions & 0 deletions packages/react-error-overlay/build.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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',
},
],
},
};
Loading