Skip to content

Commit 85bf8fd

Browse files
Timerromaindso
authored andcommitted
Improve unmapper file heuristic, add limited warning support, and ignore internal errors (facebook#2128)
* Browser sort is not stable * Fix ordering of final message * Register the warning capture * Display only createElement warnings * Use different method name * Fix regression * Ignore errors with only node_module files * Ignore null files, too * Revise count * Revise warning * Update overlay.js * Add support for facebook/react#9679 * Use absolute paths * Trim path if it's absolute * Make sure it's an absolute path * Oops * Tweak for new behavior * Make it safer * More resilient warnings * Prettier output * Fix flow
1 parent 8abf4a4 commit 85bf8fd

File tree

8 files changed

+145
-32
lines changed

8 files changed

+145
-32
lines changed

packages/react-error-overlay/src/components/frame.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,12 @@ function createFrame(
127127
lastElement: boolean
128128
) {
129129
const { compiled } = frameSetting;
130-
let { functionName } = frame;
130+
let { functionName, _originalFileName: sourceFileName } = frame;
131131
const {
132132
fileName,
133133
lineNumber,
134134
columnNumber,
135135
_scriptCode: scriptLines,
136-
_originalFileName: sourceFileName,
137136
_originalLineNumber: sourceLineNumber,
138137
_originalColumnNumber: sourceColumnNumber,
139138
_originalScriptCode: sourceLines,
@@ -149,6 +148,12 @@ function createFrame(
149148

150149
let url;
151150
if (!compiled && sourceFileName && sourceLineNumber) {
151+
// Remove everything up to the first /src/
152+
const trimMatch = /^[/|\\].*?[/|\\](src[/|\\].*)/.exec(sourceFileName);
153+
if (trimMatch && trimMatch[1]) {
154+
sourceFileName = trimMatch[1];
155+
}
156+
152157
url = sourceFileName + ':' + sourceLineNumber;
153158
if (sourceColumnNumber) {
154159
url += ':' + sourceColumnNumber;

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { SwitchCallback } from './additional';
1212

1313
function createOverlay(
1414
document: Document,
15-
name: string,
15+
name: ?string,
1616
message: string,
1717
frames: StackFrame[],
1818
contextSize: number,
@@ -52,14 +52,20 @@ function createOverlay(
5252
applyStyles(header, headerStyle);
5353

5454
// Make message prettier
55-
let finalMessage = message.match(/^\w*:/) ? message : name + ': ' + message;
55+
let finalMessage = message.match(/^\w*:/) || !name
56+
? message
57+
: name + ': ' + message;
58+
5659
finalMessage = finalMessage
5760
// TODO: maybe remove this prefix from fbjs?
5861
// It's just scaring people
59-
.replace('Invariant Violation: ', '')
62+
.replace(/^Invariant Violation:\s*/, '')
63+
// This is not helpful either:
64+
.replace(/^Warning:\s*/, '')
6065
// Break the actionable part to the next line.
6166
// AFAIK React 16+ should already do this.
62-
.replace(' Check the render method', '\n\nCheck the render method');
67+
.replace(' Check the render method', '\n\nCheck the render method')
68+
.replace(' Check your code at', '\n\nCheck your code at');
6369

6470
// Put it in the DOM
6571
header.appendChild(document.createTextNode(finalMessage));
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,48 @@
11
/* @flow */
2-
type ConsoleProxyCallback = (message: string) => void;
2+
3+
type ReactFrame = {
4+
fileName: string | null,
5+
lineNumber: number | null,
6+
functionName: string | null,
7+
};
8+
const reactFrameStack: Array<ReactFrame[]> = [];
9+
10+
export type { ReactFrame };
11+
12+
const registerReactStack = () => {
13+
// $FlowFixMe
14+
console.stack = frames => reactFrameStack.push(frames);
15+
// $FlowFixMe
16+
console.stackEnd = frames => reactFrameStack.pop();
17+
};
18+
19+
const unregisterReactStack = () => {
20+
// $FlowFixMe
21+
console.stack = undefined;
22+
// $FlowFixMe
23+
console.stackEnd = undefined;
24+
};
25+
26+
type ConsoleProxyCallback = (message: string, frames: ReactFrame[]) => void;
327
const permanentRegister = function proxyConsole(
428
type: string,
529
callback: ConsoleProxyCallback
630
) {
731
const orig = console[type];
832
console[type] = function __stack_frame_overlay_proxy_console__() {
9-
const message = [].slice.call(arguments).join(' ');
10-
callback(message);
33+
try {
34+
const message = arguments[0];
35+
if (typeof message === 'string' && reactFrameStack.length > 0) {
36+
callback(message, reactFrameStack[reactFrameStack.length - 1]);
37+
}
38+
} catch (err) {
39+
// Warnings must never crash. Rethrow with a clean stack.
40+
setTimeout(function() {
41+
throw err;
42+
});
43+
}
1144
return orig.apply(this, arguments);
1245
};
1346
};
1447

15-
export { permanentRegister };
48+
export { permanentRegister, registerReactStack, unregisterReactStack };

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import {
1919
register as registerStackTraceLimit,
2020
unregister as unregisterStackTraceLimit,
2121
} from './effects/stackTraceLimit';
22+
import {
23+
permanentRegister as permanentRegisterConsole,
24+
registerReactStack,
25+
unregisterReactStack,
26+
} from './effects/proxyConsole';
27+
import { massage as massageWarning } from './utils/warnings';
2228

2329
import {
2430
consume as consumeError,
@@ -66,7 +72,7 @@ const css = [
6672
'}',
6773
].join('\n');
6874

69-
function render(name: string, message: string, resolvedFrames: StackFrame[]) {
75+
function render(name: ?string, message: string, resolvedFrames: StackFrame[]) {
7076
disposeCurrentView();
7177

7278
const iframe = window.document.createElement('iframe');
@@ -156,6 +162,9 @@ function crash(error: Error, unhandledRejection = false) {
156162
}
157163
consumeError(error, unhandledRejection, CONTEXT_SIZE)
158164
.then(ref => {
165+
if (ref == null) {
166+
return;
167+
}
159168
errorReferences.push(ref);
160169
if (iframeReference !== null && additionalReference !== null) {
161170
updateAdditional(
@@ -205,13 +214,28 @@ function inject() {
205214
registerPromise(window, error => crash(error, true));
206215
registerShortcuts(window, shortcutHandler);
207216
registerStackTraceLimit();
217+
218+
registerReactStack();
219+
permanentRegisterConsole('error', (warning, stack) => {
220+
const data = massageWarning(warning, stack);
221+
crash(
222+
// $FlowFixMe
223+
{
224+
message: data.message,
225+
stack: data.stack,
226+
__unmap_source: '/static/js/bundle.js',
227+
},
228+
false
229+
);
230+
});
208231
}
209232

210233
function uninject() {
211234
unregisterStackTraceLimit();
212235
unregisterShortcuts(window);
213236
unregisterPromise(window);
214237
unregisterError(window);
238+
unregisterReactStack();
215239
}
216240

217241
export { inject, uninject };

packages/react-error-overlay/src/utils/errorRegister.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function consume(
1919
error: Error,
2020
unhandledRejection: boolean = false,
2121
contextSize: number = 3
22-
): Promise<ErrorRecordReference> {
22+
): Promise<ErrorRecordReference | null> {
2323
const parsedFrames = parse(error);
2424
let enhancedFramesPromise;
2525
if (error.__unmap_source) {
@@ -33,6 +33,13 @@ function consume(
3333
enhancedFramesPromise = map(parsedFrames, contextSize);
3434
}
3535
return enhancedFramesPromise.then(enhancedFrames => {
36+
if (
37+
enhancedFrames
38+
.map(f => f._originalFileName)
39+
.filter(f => f != null && f.indexOf('node_modules') === -1).length === 0
40+
) {
41+
return null;
42+
}
3643
enhancedFrames = enhancedFrames.filter(
3744
({ functionName }) =>
3845
functionName == null ||

packages/react-error-overlay/src/utils/unmapper.js

+28-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ import { getSourceMap } from './getSourceMap';
44
import { getLinesAround } from './getLinesAround';
55
import path from 'path';
66

7+
function count(search: string, string: string): number {
8+
// Count starts at -1 becuse a do-while loop always runs at least once
9+
let count = -1, index = -1;
10+
do {
11+
// First call or the while case evaluated true, meaning we have to make
12+
// count 0 or we found a character
13+
++count;
14+
// Find the index of our search string, starting after the previous index
15+
index = string.indexOf(search, index + 1);
16+
} while (index !== -1);
17+
return count;
18+
}
19+
720
/**
821
* Turns a set of mapped <code>StackFrame</code>s back into their generated code position and enhances them with code.
922
* @param {string} fileUri The URI of the <code>bundle.js</code> file.
@@ -39,28 +52,23 @@ async function unmap(
3952
return frame;
4053
}
4154
const fN: string = fileName;
42-
const splitCache1: any = {}, splitCache2: any = {}, splitCache3: any = {};
4355
const source = map
4456
.getSources()
4557
.map(s => s.replace(/[\\]+/g, '/'))
46-
.filter(s => {
47-
s = path.normalize(s);
48-
return s.indexOf(fN) === s.length - fN.length;
49-
})
50-
.sort((a, b) => {
51-
let a2 = splitCache1[a] || (splitCache1[a] = a.split(path.sep)),
52-
b2 = splitCache1[b] || (splitCache1[b] = b.split(path.sep));
53-
return Math.sign(a2.length - b2.length);
54-
})
55-
.sort((a, b) => {
56-
let a2 = splitCache2[a] || (splitCache2[a] = a.split('node_modules')),
57-
b2 = splitCache2[b] || (splitCache2[b] = b.split('node_modules'));
58-
return Math.sign(a2.length - b2.length);
58+
.filter(p => {
59+
p = path.normalize(p);
60+
const i = p.lastIndexOf(fN);
61+
return i !== -1 && i === p.length - fN.length;
5962
})
63+
.map(p => ({
64+
token: p,
65+
seps: count(path.sep, path.normalize(p)),
66+
penalties: count('node_modules', p) + count('~', p),
67+
}))
6068
.sort((a, b) => {
61-
let a2 = splitCache3[a] || (splitCache3[a] = a.split('~')),
62-
b2 = splitCache3[b] || (splitCache3[b] = b.split('~'));
63-
return Math.sign(a2.length - b2.length);
69+
const s = Math.sign(a.seps - b.seps);
70+
if (s !== 0) return s;
71+
return Math.sign(a.penalties - b.penalties);
6472
});
6573
if (source.length < 1 || lineNumber == null) {
6674
return new StackFrame(
@@ -76,13 +84,14 @@ async function unmap(
7684
null
7785
);
7886
}
87+
const sourceT = source[0].token;
7988
const { line, column } = map.getGeneratedPosition(
80-
source[0],
89+
sourceT,
8190
lineNumber,
8291
// $FlowFixMe
8392
columnNumber
8493
);
85-
const originalSource = map.getSource(source[0]);
94+
const originalSource = map.getSource(sourceT);
8695
return new StackFrame(
8796
functionName,
8897
fileUri,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// @flow
2+
import type { ReactFrame } from '../effects/proxyConsole';
3+
4+
function stripInlineStacktrace(message: string): string {
5+
return message.split('\n').filter(line => !line.match(/^\s*in/)).join('\n'); // " in Foo"
6+
}
7+
8+
function massage(
9+
warning: string,
10+
frames: ReactFrame[]
11+
): { message: string, stack: string } {
12+
let message = stripInlineStacktrace(warning);
13+
14+
// Reassemble the stack with full filenames provided by React
15+
let stack = '';
16+
for (let index = 0; index < frames.length; ++index) {
17+
const { fileName, lineNumber } = frames[index];
18+
if (fileName == null || lineNumber == null) {
19+
continue;
20+
}
21+
let { functionName } = frames[index];
22+
functionName = functionName || '(anonymous function)';
23+
stack += `in ${functionName} (at ${fileName}:${lineNumber})\n`;
24+
}
25+
26+
return { message, stack };
27+
}
28+
29+
export { massage };

packages/react-scripts/config/webpack.config.dev.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ module.exports = {
7777
publicPath: publicPath,
7878
// Point sourcemap entries to original disk location
7979
devtoolModuleFilenameTemplate: info =>
80-
path.relative(paths.appSrc, info.absoluteResourcePath),
80+
path.resolve(info.absoluteResourcePath),
8181
},
8282
resolve: {
8383
// This allows you to set a fallback for where Webpack should look for modules.

0 commit comments

Comments
 (0)