mirror of
https://github.com/zhigang1992/create-react-app.git
synced 2026-01-12 22:46:30 +08:00
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
This commit is contained in:
committed by
GitHub
parent
01a0d737c7
commit
cd3d04b71e
95
packages/react-error-overlay/build.js
vendored
Normal file
95
packages/react-error-overlay/build.js
vendored
Normal file
@@ -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();
|
||||
});
|
||||
@@ -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": [
|
||||
|
||||
57
packages/react-error-overlay/src/iframeScript.js
vendored
Normal file
57
packages/react-error-overlay/src/iframeScript.js
vendored
Normal file
@@ -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();
|
||||
89
packages/react-error-overlay/src/index.js
vendored
89
packages/react-error-overlay/src/index.js
vendored
@@ -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<any> = null;
|
||||
let currentBuildError: null | string = null;
|
||||
let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
|
||||
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 <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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateIframeContent() {
|
||||
if (iframe === null) {
|
||||
if (!currentRuntimeErrorOptions) {
|
||||
throw new Error('Expected options to be injected.');
|
||||
}
|
||||
|
||||
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
packages/react-error-overlay/src/utils/pollyfills.js
vendored
Normal file
18
packages/react-error-overlay/src/utils/pollyfills.js
vendored
Normal file
@@ -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
packages/react-error-overlay/webpack.config.iframe.js
Normal file
27
packages/react-error-overlay/webpack.config.iframe.js
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
38
packages/react-error-overlay/webpack.config.js
Normal file
38
packages/react-error-overlay/webpack.config.js
Normal file
@@ -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'),
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user