mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: This diff adds handling for syntax errors. ## Strategy To do this we introduce a new log level type syntax, giving us these levels with semantics: - `warn` - console warns, show collapsed, dismissible - `error` - console errors, show collapsed, dismissible - `fatal` - thrown exceptions, show expanded, not dismissible - `syntax` - thrown exceptions for invalid syntax, show expanded, not dismissible Syntax errors shows expanded, covers all other errors, and are only dismissible when the syntax error is fixed and updated with Fast Refresh. Once the syntax error is fixed, it reveals any previously covered fatals, errors, or warnings behind it In many ways, this makes syntax errors the highest level error. ## Visuals Syntax errors also have their own display formatting. Stack traces for syntax errors don't make sense, so we don't show them. Instead, we show the syntax error message and a code frame for the error. The code frame is also updated so that is doesn't wrap and is horizontally scrollable, making it easier to read. ## Detecting syntax errors To detect syntax errors we've updated `LogBoxData.addException` to call the parse function `parseLogBoxException`. This method will perform a regex on the error message to detect: - file name - location - error message - codeframe If this regex fails for any reason to find all four parts, we'll fall back to a fatal. Over time we'll update this regex to be more robust and handle more cases we've missed. Changelog: [Internal] Reviewed By: motiz88 Differential Revision: D18278862 fbshipit-source-id: 59069aba38a27c44787e5248b2973c3a345c4a0a
160 lines
4.1 KiB
JavaScript
160 lines
4.1 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow strict-local
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import * as React from 'react';
|
|
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
|
|
import StyleSheet from '../../StyleSheet/StyleSheet';
|
|
import View from '../../Components/View/View';
|
|
import LogBoxInspector from './LogBoxInspector';
|
|
import LogBoxLog from '../Data/LogBoxLog';
|
|
import LogBoxLogNotification from './LogBoxLogNotification';
|
|
import type {LogBoxLogs} from '../Data/LogBoxData';
|
|
|
|
type Props = $ReadOnly<{|
|
|
onDismiss: (log: LogBoxLog) => void,
|
|
onDismissWarns: () => void,
|
|
onDismissErrors: () => void,
|
|
logs: LogBoxLogs,
|
|
|}>;
|
|
|
|
function LogBoxContainer(props: Props): React.Node {
|
|
const [selectedLogIndex, setSelectedLog] = React.useState(null);
|
|
|
|
const logs = Array.from(props.logs);
|
|
|
|
function handleInspectorDismiss() {
|
|
// Here we handle the cases when the log is dismissed and it
|
|
// was either the last log, or when the current index
|
|
// is now outside the bounds of the log array.
|
|
if (selectedLogIndex != null) {
|
|
if (logs.length - 1 <= 0) {
|
|
setSelectedLog(null);
|
|
} else if (selectedLogIndex >= logs.length - 1) {
|
|
setSelectedLog(selectedLogIndex - 1);
|
|
}
|
|
props.onDismiss(logs[selectedLogIndex]);
|
|
}
|
|
}
|
|
|
|
function handleInspectorMinimize() {
|
|
setSelectedLog(null);
|
|
}
|
|
|
|
function openLog(log: LogBoxLog) {
|
|
let index = logs.length - 1;
|
|
|
|
// Stop at zero because if we don't find any log, we'll open the first log.
|
|
while (index > 0 && logs[index] !== log) {
|
|
index -= 1;
|
|
}
|
|
setSelectedLog(index);
|
|
}
|
|
|
|
let fatalIndex = logs.length - 1;
|
|
while (fatalIndex >= 0) {
|
|
const level = logs[fatalIndex].level;
|
|
if (level === 'fatal' || level === 'syntax') {
|
|
break;
|
|
}
|
|
fatalIndex -= 1;
|
|
}
|
|
|
|
if (selectedLogIndex == null && fatalIndex >= 0) {
|
|
return (
|
|
<View style={StyleSheet.absoluteFill}>
|
|
<LogBoxInspector
|
|
onDismiss={handleInspectorDismiss}
|
|
onMinimize={handleInspectorMinimize}
|
|
onChangeSelectedIndex={setSelectedLog}
|
|
logs={logs}
|
|
hasFatal={true}
|
|
selectedIndex={fatalIndex}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (selectedLogIndex != null) {
|
|
return (
|
|
<View style={StyleSheet.absoluteFill}>
|
|
<LogBoxInspector
|
|
onDismiss={handleInspectorDismiss}
|
|
onMinimize={handleInspectorMinimize}
|
|
onChangeSelectedIndex={setSelectedLog}
|
|
logs={logs}
|
|
hasFatal={fatalIndex != null}
|
|
selectedIndex={selectedLogIndex}
|
|
/>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (logs.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const warnings = logs.filter(log => log.level === 'warn');
|
|
const errors = logs.filter(log => log.level === 'error');
|
|
return (
|
|
<View style={styles.list}>
|
|
{warnings.length > 0 && (
|
|
<View style={styles.toast}>
|
|
<LogBoxLogNotification
|
|
log={warnings[warnings.length - 1]}
|
|
level="warn"
|
|
totalLogCount={warnings.length}
|
|
onPressOpen={() => openLog(warnings[warnings.length - 1])}
|
|
onPressList={() => {
|
|
/* TODO: open log list */
|
|
}}
|
|
onPressDismiss={props.onDismissWarns}
|
|
/>
|
|
</View>
|
|
)}
|
|
{errors.length > 0 && (
|
|
<View style={styles.toast}>
|
|
<LogBoxLogNotification
|
|
log={errors[errors.length - 1]}
|
|
level="error"
|
|
totalLogCount={errors.length}
|
|
onPressOpen={() => openLog(errors[errors.length - 1])}
|
|
onPressList={() => {
|
|
/* TODO: open log list */
|
|
}}
|
|
onPressDismiss={props.onDismissErrors}
|
|
/>
|
|
</View>
|
|
)}
|
|
<SafeAreaView style={styles.safeArea} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
list: {
|
|
bottom: 10,
|
|
left: 10,
|
|
right: 10,
|
|
position: 'absolute',
|
|
},
|
|
toast: {
|
|
borderRadius: 8,
|
|
marginBottom: 5,
|
|
overflow: 'hidden',
|
|
},
|
|
safeArea: {
|
|
flex: 1,
|
|
},
|
|
});
|
|
|
|
export default LogBoxContainer;
|