index.js 4.9 KB
Newer Older
Dan Abramov's avatar
Dan Abramov committed
1
2
3
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
Sophie Alpert's avatar
Sophie Alpert committed
4
5
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
Dan Abramov's avatar
Dan Abramov committed
6
7
 */

8
/* @flow */
9
import { listenToRuntimeErrors } from './listenToRuntimeErrors';
10
import { iframeStyle } from './styles';
11
import { applyStyles } from './utils/dom/css';
12

13
14
15
16
17
// 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';

18
import type { ErrorRecord } from './listenToRuntimeErrors';
19
import type { ErrorLocation } from './utils/parseCompileError';
20
21
22

type RuntimeReportingOptions = {|
  onError: () => void,
23
  filename?: string,
24
25
|};

26
27
type EditorHandler = (errorLoc: ErrorLocation) => void;

28
29
let iframe: null | HTMLIFrameElement = null;
let isLoadingIframe: boolean = false;
30
var isIframeReady: boolean = false;
31

32
let editorHandler: null | EditorHandler = null;
33
34
35
36
37
let currentBuildError: null | string = null;
let currentRuntimeErrorRecords: Array<ErrorRecord> = [];
let currentRuntimeErrorOptions: null | RuntimeReportingOptions = null;
let stopListeningToRuntimeErrors: null | (() => void) = null;

38
39
40
41
42
43
44
export function setEditorHandler(handler: EditorHandler | null) {
  editorHandler = handler;
  if (iframe) {
    update();
  }
}

45
46
47
48
49
50
51
52
53
54
55
56
57
58
export function reportBuildError(error: string) {
  currentBuildError = error;
  update();
}

export function dismissBuildError() {
  currentBuildError = null;
  update();
}

export function startReportingRuntimeErrors(options: RuntimeReportingOptions) {
  if (stopListeningToRuntimeErrors !== null) {
    throw new Error('Already listening');
  }
59
60
61
62
63
64
65
  if (options.launchEditorEndpoint) {
    console.warn(
      'Warning: `startReportingRuntimeErrors` doesn’t accept ' +
        '`launchEditorEndpoint` argument anymore. Use `listenToOpenInEditor` ' +
        'instead with your own implementation to open errors in editor '
    );
  }
66
  currentRuntimeErrorOptions = options;
67
  stopListeningToRuntimeErrors = listenToRuntimeErrors(errorRecord => {
68
69
70
71
72
73
74
    try {
      if (typeof options.onError === 'function') {
        options.onError.call(null);
      }
    } finally {
      handleRuntimeError(errorRecord);
    }
75
  }, options.filename);
76
}
77
78
79
80
81
82
83
84
85
86
87
88
89

function handleRuntimeError(errorRecord) {
  if (
    currentRuntimeErrorRecords.some(({ error }) => error === errorRecord.error)
  ) {
    // Deduplicate identical errors.
    // This fixes https://github.com/facebookincubator/create-react-app/issues/3011.
    return;
  }
  currentRuntimeErrorRecords = currentRuntimeErrorRecords.concat([errorRecord]);
  update();
}

90
export function dismissRuntimeErrors() {
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
  currentRuntimeErrorRecords = [];
  update();
}

export function stopReportingRuntimeErrors() {
  if (stopListeningToRuntimeErrors === null) {
    throw new Error('Not currently listening');
  }
  currentRuntimeErrorOptions = null;
  try {
    stopListeningToRuntimeErrors();
  } finally {
    stopListeningToRuntimeErrors = null;
  }
}

function update() {
  // 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;
  }
114
115
  if (isIframeReady) {
    // Iframe is ready.
116
117
118
119
120
121
122
123
124
125
126
127
    // Just update it.
    updateIframeContent();
    return;
  }
  // We need to schedule the first render.
  isLoadingIframe = true;
  const loadingIframe = window.document.createElement('iframe');
  applyStyles(loadingIframe, iframeStyle);
  loadingIframe.onload = function() {
    const iframeDocument = loadingIframe.contentDocument;
    if (iframeDocument != null && iframeDocument.body != null) {
      iframe = loadingIframe;
128
129
130
131
132
133
      const script = loadingIframe.contentWindow.document.createElement(
        'script'
      );
      script.type = 'text/javascript';
      script.innerHTML = iframeScript;
      iframeDocument.body.appendChild(script);
134
135
136
137
138
139
    }
  };
  const appDocument = window.document;
  appDocument.body.appendChild(loadingIframe);
}

140
141
142
function updateIframeContent() {
  if (!currentRuntimeErrorOptions) {
    throw new Error('Expected options to be injected.');
143
144
  }

145
  if (!iframe) {
146
147
    throw new Error('Iframe has not been created yet.');
  }
148
149
150
151
152

  const isRendered = iframe.contentWindow.updateContent({
    currentBuildError,
    currentRuntimeErrorRecords,
    dismissRuntimeErrors,
153
    editorHandler,
154
155
156
  });

  if (!isRendered) {
157
158
    window.document.body.removeChild(iframe);
    iframe = null;
159
    isIframeReady = false;
160
161
  }
}
162
163
164
165
166
167
168
169

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();
};
170
171
172
173
174
175
176

if (process.env.NODE_ENV === 'production') {
  console.warn(
    'react-error-overlay is not meant for use in production. You should ' +
      'ensure it is not included in your build to reduce bundle size.'
  );
}