mirror of
https://github.com/status-im/react-native.git
synced 2025-01-14 03:26:07 +00:00
RN: A wild YellowBox has appeared!
Summary: Replaces the existing `YellowBox` with a modern one. Here are the notable changes: - Sort warnings by recency (with most recent on top). - Group warnings by format string if present. - Present stack traces similar to RedBox. - Show status of loading source maps. - Support inspecting each occurrence of a warning. - Fixed a bunch of edge cases and race conditions. Reviewed By: TheSavior Differential Revision: D8345180 fbshipit-source-id: b9e10d526b262c3985bbea639ba2ea0e7cad5081
This commit is contained in:
parent
f8b4850425
commit
d0219a0301
@ -127,4 +127,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
if (!global.__RCTProfileIsProfiling) {
|
||||
const YellowBox = require('YellowBox');
|
||||
YellowBox.install();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppContainer;
|
||||
|
@ -1,554 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('EventEmitter');
|
||||
const Platform = require('Platform');
|
||||
const React = require('React');
|
||||
const SafeAreaView = require('SafeAreaView');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const RCTLog = require('RCTLog');
|
||||
|
||||
const infoLog = require('infoLog');
|
||||
const openFileInEditor = require('openFileInEditor');
|
||||
const parseErrorStack = require('parseErrorStack');
|
||||
const stringifySafe = require('stringifySafe');
|
||||
const symbolicateStackTrace = require('symbolicateStackTrace');
|
||||
|
||||
import type EmitterSubscription from 'EmitterSubscription';
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
type WarningInfo = {
|
||||
count: number,
|
||||
stacktrace: Array<StackFrame>,
|
||||
symbolicated: boolean,
|
||||
};
|
||||
|
||||
const _warningEmitter = new EventEmitter();
|
||||
const _warningMap: Map<string, WarningInfo> = new Map();
|
||||
const IGNORED_WARNINGS: Array<string> = [];
|
||||
|
||||
/**
|
||||
* YellowBox renders warnings at the bottom of the app being developed.
|
||||
*
|
||||
* Warnings help guard against subtle yet significant issues that can impact the
|
||||
* quality of the app. This "in your face" style of warning allows developers to
|
||||
* notice and correct these issues as quickly as possible.
|
||||
*
|
||||
* By default, the warning box is enabled in `__DEV__`. Set the following flag
|
||||
* to disable it (and call `console.warn` to update any rendered <YellowBox>):
|
||||
*
|
||||
* console.disableYellowBox = true;
|
||||
* console.warn('YellowBox is disabled.');
|
||||
*
|
||||
* Ignore specific warnings by calling:
|
||||
*
|
||||
* YellowBox.ignoreWarnings(['Warning: ...']);
|
||||
*
|
||||
* (DEPRECATED) Warnings can be ignored programmatically by setting the array:
|
||||
*
|
||||
* console.ignoredYellowBox = ['Warning: ...'];
|
||||
*
|
||||
* Strings in `console.ignoredYellowBox` can be a prefix of the warning that
|
||||
* should be ignored.
|
||||
*/
|
||||
|
||||
if (__DEV__) {
|
||||
const {error, warn} = console;
|
||||
|
||||
(console: any).error = function() {
|
||||
error.apply(console, arguments);
|
||||
// Show yellow box for the `warning` module.
|
||||
if (
|
||||
typeof arguments[0] === 'string' &&
|
||||
arguments[0].startsWith('Warning: ')
|
||||
) {
|
||||
updateWarningMap.apply(null, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
(console: any).warn = function() {
|
||||
warn.apply(console, arguments);
|
||||
updateWarningMap.apply(null, arguments);
|
||||
};
|
||||
|
||||
if (Platform.isTesting) {
|
||||
(console: any).disableYellowBox = true;
|
||||
}
|
||||
|
||||
RCTLog.setWarningHandler((...args) => {
|
||||
updateWarningMap.apply(null, args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple function for formatting strings.
|
||||
*
|
||||
* Replaces placeholders with values passed as extra arguments
|
||||
*
|
||||
* @param {string} format the base string
|
||||
* @param ...args the values to insert
|
||||
* @return {string} the replaced string
|
||||
*/
|
||||
function sprintf(format, ...args) {
|
||||
let index = 0;
|
||||
return format.replace(/%s/g, match => args[index++]);
|
||||
}
|
||||
|
||||
function updateWarningMap(...args): void {
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an error
|
||||
* found when Flow v0.68 was deployed. To see the error delete this comment
|
||||
* and run Flow. */
|
||||
if (console.disableYellowBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
let warning;
|
||||
if (typeof args[0] === 'string') {
|
||||
const [format, ...formatArgs] = args;
|
||||
const argCount = (format.match(/%s/g) || []).length;
|
||||
warning = [
|
||||
sprintf(format, ...formatArgs.slice(0, argCount).map(stringifySafe)),
|
||||
...formatArgs.slice(argCount).map(stringifySafe),
|
||||
].join(' ');
|
||||
} else {
|
||||
warning = args.map(stringifySafe).join(' ');
|
||||
}
|
||||
|
||||
if (warning.startsWith('(ADVICE)')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warningInfo = _warningMap.get(warning);
|
||||
if (warningInfo) {
|
||||
warningInfo.count += 1;
|
||||
} else {
|
||||
const error: any = new Error();
|
||||
error.framesToPop = 2;
|
||||
|
||||
_warningMap.set(warning, {
|
||||
count: 1,
|
||||
stacktrace: parseErrorStack(error),
|
||||
symbolicated: false,
|
||||
});
|
||||
}
|
||||
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
|
||||
function ensureSymbolicatedWarning(warning: string): void {
|
||||
const prevWarningInfo = _warningMap.get(warning);
|
||||
if (!prevWarningInfo || prevWarningInfo.symbolicated) {
|
||||
return;
|
||||
}
|
||||
prevWarningInfo.symbolicated = true;
|
||||
|
||||
symbolicateStackTrace(prevWarningInfo.stacktrace).then(
|
||||
stack => {
|
||||
const nextWarningInfo = _warningMap.get(warning);
|
||||
if (nextWarningInfo) {
|
||||
nextWarningInfo.stacktrace = stack;
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
const nextWarningInfo = _warningMap.get(warning);
|
||||
if (nextWarningInfo) {
|
||||
infoLog('Failed to symbolicate warning, "%s":', warning, error);
|
||||
_warningEmitter.emit('warning', _warningMap);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function isWarningIgnored(warning: string): boolean {
|
||||
const isIgnored = IGNORED_WARNINGS.some((ignoredWarning: string) =>
|
||||
warning.includes(ignoredWarning),
|
||||
);
|
||||
|
||||
if (isIgnored) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
return (
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
Array.isArray(console.ignoredYellowBox) &&
|
||||
console.ignoredYellowBox.some(ignorePrefix =>
|
||||
warning.startsWith(String(ignorePrefix)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const WarningRow = ({count, warning, onPress}) => {
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const View = require('View');
|
||||
|
||||
const countText =
|
||||
count > 1 ? (
|
||||
<Text style={styles.listRowCount}>{'(' + count + ') '}</Text>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={styles.listRow}>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onPress}
|
||||
style={styles.listRowContent}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.listRowText} numberOfLines={2}>
|
||||
{countText}
|
||||
{warning}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type StackRowProps = {frame: StackFrame};
|
||||
const StackRow = ({frame}: StackRowProps) => {
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const {file, lineNumber} = frame;
|
||||
let fileName;
|
||||
if (file) {
|
||||
const fileParts = file.split('/');
|
||||
fileName = fileParts[fileParts.length - 1];
|
||||
} else {
|
||||
fileName = '<unknown file>';
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
style={styles.openInEditorButton}
|
||||
underlayColor="transparent"
|
||||
onPress={openFileInEditor.bind(null, file, lineNumber)}>
|
||||
<Text style={styles.inspectorCountText}>
|
||||
{fileName}:{lineNumber}
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
const WarningInspector = ({
|
||||
warningInfo,
|
||||
warning,
|
||||
stacktraceVisible,
|
||||
onDismiss,
|
||||
onMinimize,
|
||||
toggleStacktrace,
|
||||
}) => {
|
||||
const ScrollView = require('ScrollView');
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
const View = require('View');
|
||||
const {count, stacktrace} = warningInfo || {};
|
||||
|
||||
const countSentence =
|
||||
'Warning encountered ' + count + ' time' + (count - 1 ? 's' : '') + '.';
|
||||
|
||||
let stacktraceList;
|
||||
if (stacktraceVisible && stacktrace) {
|
||||
stacktraceList = (
|
||||
<View style={styles.stacktraceList}>
|
||||
{stacktrace.map((frame, ii) => <StackRow frame={frame} key={ii} />)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.inspector}>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.inspectorCount}>
|
||||
<Text style={styles.inspectorCountText}>{countSentence}</Text>
|
||||
<TouchableHighlight
|
||||
onPress={toggleStacktrace}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>
|
||||
{stacktraceVisible ? '\u{25BC}' : '\u{25B6}'} Stacktrace
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<ScrollView style={styles.inspectorWarning}>
|
||||
{stacktraceList}
|
||||
<Text style={styles.inspectorWarningText}>{warning}</Text>
|
||||
</ScrollView>
|
||||
<View style={styles.inspectorButtons}>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onMinimize}
|
||||
style={styles.inspectorButton}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>Minimize</Text>
|
||||
</TouchableHighlight>
|
||||
<TouchableHighlight
|
||||
activeOpacity={0.5}
|
||||
onPress={onDismiss}
|
||||
style={styles.inspectorButton}
|
||||
underlayColor="transparent">
|
||||
<Text style={styles.inspectorButtonText}>Dismiss</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
class YellowBox extends React.Component<
|
||||
mixed,
|
||||
{
|
||||
stacktraceVisible: boolean,
|
||||
inspecting: ?string,
|
||||
warningMap: Map<any, any>,
|
||||
},
|
||||
> {
|
||||
_listener: ?EmitterSubscription;
|
||||
dismissWarning: (warning: ?string) => void;
|
||||
|
||||
constructor(props: mixed, context: mixed) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
inspecting: null,
|
||||
stacktraceVisible: false,
|
||||
warningMap: _warningMap,
|
||||
};
|
||||
this.dismissWarning = warning => {
|
||||
const {inspecting, warningMap} = this.state;
|
||||
if (warning) {
|
||||
warningMap.delete(warning);
|
||||
} else {
|
||||
warningMap.clear();
|
||||
}
|
||||
this.setState({
|
||||
inspecting: warning && inspecting !== warning ? inspecting : null,
|
||||
warningMap,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
static ignoreWarnings(warnings: Array<string>): void {
|
||||
warnings.forEach((warning: string) => {
|
||||
if (IGNORED_WARNINGS.indexOf(warning) === -1) {
|
||||
IGNORED_WARNINGS.push(warning);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let scheduled = null;
|
||||
this._listener = _warningEmitter.addListener('warning', warningMap => {
|
||||
// Use `setImmediate` because warnings often happen during render, but
|
||||
// state cannot be set while rendering.
|
||||
scheduled =
|
||||
scheduled ||
|
||||
setImmediate(() => {
|
||||
scheduled = null;
|
||||
this.setState({
|
||||
warningMap,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const {inspecting} = this.state;
|
||||
if (inspecting != null) {
|
||||
ensureSymbolicatedWarning(inspecting);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._listener) {
|
||||
this._listener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
/* $FlowFixMe(>=0.68.0 site=react_native_fb) This comment suppresses an
|
||||
* error found when Flow v0.68 was deployed. To see the error delete this
|
||||
* comment and run Flow. */
|
||||
if (console.disableYellowBox || this.state.warningMap.size === 0) {
|
||||
return null;
|
||||
}
|
||||
const ScrollView = require('ScrollView');
|
||||
const View = require('View');
|
||||
const Text = require('Text');
|
||||
const TouchableHighlight = require('TouchableHighlight');
|
||||
|
||||
const {inspecting, stacktraceVisible} = this.state;
|
||||
const inspector =
|
||||
inspecting !== null ? (
|
||||
<WarningInspector
|
||||
warningInfo={this.state.warningMap.get(inspecting)}
|
||||
warning={inspecting}
|
||||
stacktraceVisible={stacktraceVisible}
|
||||
onDismiss={() => this.dismissWarning(inspecting)}
|
||||
onMinimize={() => this.setState({inspecting: null})}
|
||||
toggleStacktrace={() =>
|
||||
this.setState({stacktraceVisible: !stacktraceVisible})
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const rows = [];
|
||||
this.state.warningMap.forEach((warningInfo, warning) => {
|
||||
if (!isWarningIgnored(warning)) {
|
||||
rows.push(
|
||||
<WarningRow
|
||||
key={warning}
|
||||
count={warningInfo.count}
|
||||
warning={warning}
|
||||
onPress={() => this.setState({inspecting: warning})}
|
||||
onDismiss={() => this.dismissWarning(warning)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const listStyle = [
|
||||
styles.list,
|
||||
// Additional `0.4` so the 5th row can peek into view.
|
||||
{height: Math.min(rows.length, 4.4) * (rowGutter + rowHeight)},
|
||||
];
|
||||
return (
|
||||
<View style={inspector ? styles.fullScreen : listStyle}>
|
||||
{!inspector &&
|
||||
rows.length > 0 && (
|
||||
<TouchableHighlight
|
||||
style={styles.dismissAllContainer}
|
||||
onPress={() => this.dismissWarning(null)}>
|
||||
<Text style={styles.dismissAll}>Dismiss All</Text>
|
||||
</TouchableHighlight>
|
||||
)}
|
||||
<ScrollView style={listStyle} scrollsToTop={false}>
|
||||
{rows}
|
||||
</ScrollView>
|
||||
{inspector}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundColor = opacity => 'rgba(250, 186, 48, ' + opacity + ')';
|
||||
const textColor = 'white';
|
||||
const rowGutter = 1;
|
||||
const rowHeight = 46;
|
||||
|
||||
// For unknown reasons, setting elevation: Number.MAX_VALUE causes remote debugging to
|
||||
// hang on iOS (some sort of overflow maybe). Setting it to Number.MAX_SAFE_INTEGER fixes the iOS issue, but since
|
||||
// elevation is an android-only style property we might as well remove it altogether for iOS.
|
||||
// See: https://github.com/facebook/react-native/issues/12223
|
||||
const elevation =
|
||||
Platform.OS === 'android' ? Number.MAX_SAFE_INTEGER : undefined;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fullScreen: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
elevation: elevation,
|
||||
position: 'absolute',
|
||||
},
|
||||
inspector: {
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
height: '100%',
|
||||
paddingTop: 5,
|
||||
elevation: elevation,
|
||||
},
|
||||
inspectorButtons: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
inspectorButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 22,
|
||||
backgroundColor: backgroundColor(1),
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
stacktraceList: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
inspectorButtonText: {
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
opacity: 0.8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
openInEditorButton: {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
},
|
||||
inspectorCount: {
|
||||
padding: 15,
|
||||
paddingBottom: 0,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
inspectorCountText: {
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
inspectorWarning: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
inspectorWarningText: {
|
||||
color: textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
list: {
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
elevation: elevation,
|
||||
},
|
||||
listRow: {
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
height: rowHeight,
|
||||
marginTop: rowGutter,
|
||||
},
|
||||
listRowContent: {
|
||||
flex: 1,
|
||||
},
|
||||
listRowCount: {
|
||||
color: 'rgba(255, 255, 255, 0.5)',
|
||||
},
|
||||
listRowText: {
|
||||
color: textColor,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: Platform.OS === 'android' ? 5 : 7,
|
||||
marginLeft: 15,
|
||||
marginRight: 15,
|
||||
},
|
||||
dismissAllContainer: {
|
||||
height: 20,
|
||||
justifyContent: 'center',
|
||||
marginTop: -30,
|
||||
marginRight: 5,
|
||||
backgroundColor: backgroundColor(0.95),
|
||||
alignSelf: 'flex-end',
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 10,
|
||||
},
|
||||
dismissAll: {
|
||||
color: 'white',
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBox;
|
@ -92,10 +92,10 @@ export type ____LayoutStyle_Internal = $ReadOnly<{|
|
||||
export type ____TransformStyle_Internal = $ReadOnly<{|
|
||||
transform?: $ReadOnlyArray<
|
||||
| {|+perspective: number | AnimatedNode|}
|
||||
| {|+rotate: string|}
|
||||
| {|+rotateX: string|}
|
||||
| {|+rotateY: string|}
|
||||
| {|+rotateZ: string|}
|
||||
| {|+rotate: string | AnimatedNode|}
|
||||
| {|+rotateX: string | AnimatedNode|}
|
||||
| {|+rotateY: string | AnimatedNode|}
|
||||
| {|+rotateZ: string | AnimatedNode|}
|
||||
| {|+scale: number | AnimatedNode|}
|
||||
| {|+scaleX: number | AnimatedNode|}
|
||||
| {|+scaleY: number | AnimatedNode|}
|
||||
|
@ -20,6 +20,7 @@ const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationI
|
||||
* - Less chance of typos.
|
||||
*/
|
||||
const UTFSequence = deepFreezeAndThrowOnMutationInDev({
|
||||
BOM: '\ufeff', // byte order mark
|
||||
BULLET: '\u2022', // bullet: •
|
||||
BULLET_SP: '\u00A0\u2022\u00A0', // •
|
||||
MIDDOT: '\u00B7', // normal middle dot: ·
|
||||
@ -31,6 +32,8 @@ const UTFSequence = deepFreezeAndThrowOnMutationInDev({
|
||||
NDASH_SP: '\u00A0\u2013\u00A0', // –
|
||||
NBSP: '\u00A0', // non-breaking space:
|
||||
PIZZA: '\uD83C\uDF55',
|
||||
TRIANGLE_LEFT: '\u25c0', // black left-pointing triangle
|
||||
TRIANGLE_RIGHT: '\u25b6', // black right-pointing triangle
|
||||
});
|
||||
|
||||
module.exports = UTFSequence;
|
||||
|
146
Libraries/YellowBox/Data/YellowBoxCategory.js
Normal file
146
Libraries/YellowBox/Data/YellowBoxCategory.js
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const Text = require('Text');
|
||||
const UTFSequence = require('UTFSequence');
|
||||
|
||||
const stringifySafe = require('stringifySafe');
|
||||
|
||||
import type {TextStyleProp} from 'StyleSheet';
|
||||
|
||||
export type Category = string;
|
||||
export type Message = $ReadOnly<{|
|
||||
content: string,
|
||||
substitutions: $ReadOnlyArray<
|
||||
$ReadOnly<{|
|
||||
length: number,
|
||||
offset: number,
|
||||
|}>,
|
||||
>,
|
||||
|}>;
|
||||
|
||||
const SUBSTITUTION = UTFSequence.BOM + '%s';
|
||||
|
||||
const YellowBoxCategory = {
|
||||
parse(
|
||||
args: $ReadOnlyArray<mixed>,
|
||||
): $ReadOnly<{|
|
||||
category: Category,
|
||||
message: Message,
|
||||
|}> {
|
||||
const categoryParts = [];
|
||||
const contentParts = [];
|
||||
const substitutionOffsets = [];
|
||||
|
||||
const remaining = [...args];
|
||||
|
||||
if (typeof remaining[0] === 'string') {
|
||||
const formatString = String(remaining.shift());
|
||||
const formatStringParts = formatString.split('%s');
|
||||
const substitutionCount = formatStringParts.length - 1;
|
||||
const substitutions = remaining.splice(0, substitutionCount);
|
||||
|
||||
let categoryString = '';
|
||||
let contentString = '';
|
||||
|
||||
let substitutionIndex = 0;
|
||||
for (const formatStringPart of formatStringParts) {
|
||||
categoryString += formatStringPart;
|
||||
contentString += formatStringPart;
|
||||
|
||||
if (substitutionIndex < substitutionCount) {
|
||||
if (substitutionIndex < substitutions.length) {
|
||||
const substitution = stringifySafe(
|
||||
substitutions[substitutionIndex],
|
||||
);
|
||||
substitutionOffsets.push({
|
||||
length: substitution.length,
|
||||
offset: contentString.length,
|
||||
});
|
||||
|
||||
categoryString += SUBSTITUTION;
|
||||
contentString += substitution;
|
||||
} else {
|
||||
substitutionOffsets.push({
|
||||
length: 2,
|
||||
offset: contentString.length,
|
||||
});
|
||||
|
||||
categoryString += '%s';
|
||||
contentString += '%s';
|
||||
}
|
||||
|
||||
substitutionIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
categoryParts.push(categoryString);
|
||||
contentParts.push(contentString);
|
||||
}
|
||||
|
||||
const remainingArgs = remaining.map(stringifySafe);
|
||||
categoryParts.push(...remainingArgs);
|
||||
contentParts.push(...remainingArgs);
|
||||
|
||||
return {
|
||||
category: categoryParts.join(' '),
|
||||
message: {
|
||||
content: contentParts.join(' '),
|
||||
substitutions: substitutionOffsets,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
render(
|
||||
{content, substitutions}: Message,
|
||||
substitutionStyle: TextStyleProp,
|
||||
): React.Node {
|
||||
const elements = [];
|
||||
|
||||
const lastOffset = substitutions.reduce(
|
||||
(prevOffset, substitution, index) => {
|
||||
const key = String(index);
|
||||
|
||||
if (substitution.offset > prevOffset) {
|
||||
const prevPart = content.substr(
|
||||
prevOffset,
|
||||
substitution.offset - prevOffset,
|
||||
);
|
||||
elements.push(<Text key={key}>{prevPart}</Text>);
|
||||
}
|
||||
|
||||
const substititionPart = content.substr(
|
||||
substitution.offset,
|
||||
substitution.length,
|
||||
);
|
||||
elements.push(
|
||||
<Text key={key + '.5'} style={substitutionStyle}>
|
||||
{substititionPart}
|
||||
</Text>,
|
||||
);
|
||||
|
||||
return substitution.offset + substitution.length;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
if (lastOffset < content.length - 1) {
|
||||
const lastPart = content.substr(lastOffset);
|
||||
elements.push(<Text key="-1">{lastPart}</Text>);
|
||||
}
|
||||
|
||||
return elements;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = YellowBoxCategory;
|
141
Libraries/YellowBox/Data/YellowBoxRegistry.js
Normal file
141
Libraries/YellowBox/Data/YellowBoxRegistry.js
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBoxWarning = require('YellowBoxWarning');
|
||||
|
||||
import type {Category} from 'YellowBoxCategory';
|
||||
|
||||
export type Registry = Map<Category, $ReadOnlyArray<YellowBoxWarning>>;
|
||||
|
||||
export type Observer = (registry: Registry) => void;
|
||||
|
||||
export type Subscription = $ReadOnly<{|
|
||||
unsubscribe: () => void,
|
||||
|}>;
|
||||
|
||||
const observers: Set<{observer: Observer}> = new Set();
|
||||
const ignorePatterns: Set<string> = new Set();
|
||||
const registry: Registry = new Map();
|
||||
|
||||
let disabled = false;
|
||||
let projection = new Map();
|
||||
let updateTimeout = null;
|
||||
|
||||
function isWarningIgnored(warning: YellowBoxWarning): boolean {
|
||||
for (const pattern of ignorePatterns) {
|
||||
if (warning.message.content.includes(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleUpdate(): void {
|
||||
projection = new Map();
|
||||
if (!disabled) {
|
||||
for (const [category, warnings] of registry) {
|
||||
const filtered = warnings.filter(warning => !isWarningIgnored(warning));
|
||||
if (filtered.length > 0) {
|
||||
projection.set(category, filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateTimeout == null) {
|
||||
updateTimeout = setImmediate(() => {
|
||||
updateTimeout = null;
|
||||
for (const {observer} of observers) {
|
||||
observer(projection);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const YellowBoxRegistry = {
|
||||
add({
|
||||
args,
|
||||
framesToPop,
|
||||
}: $ReadOnly<{|
|
||||
args: $ReadOnlyArray<mixed>,
|
||||
framesToPop: number,
|
||||
|}>): void {
|
||||
if (typeof args[0] === 'string' && args[0].startsWith('(ADVICE)')) {
|
||||
return;
|
||||
}
|
||||
const {category, message, stack} = YellowBoxWarning.parse({
|
||||
args,
|
||||
framesToPop: framesToPop + 1,
|
||||
});
|
||||
|
||||
let warnings = registry.get(category);
|
||||
if (warnings == null) {
|
||||
warnings = [];
|
||||
}
|
||||
warnings = [...warnings, new YellowBoxWarning(message, stack)];
|
||||
|
||||
registry.delete(category);
|
||||
registry.set(category, warnings);
|
||||
|
||||
handleUpdate();
|
||||
},
|
||||
|
||||
delete(category: Category): void {
|
||||
if (registry.has(category)) {
|
||||
registry.delete(category);
|
||||
handleUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
clear(): void {
|
||||
if (registry.size > 0) {
|
||||
registry.clear();
|
||||
handleUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
addIgnorePatterns(patterns: $ReadOnlyArray<string>): void {
|
||||
const newPatterns = patterns.filter(
|
||||
pattern => !ignorePatterns.has(pattern),
|
||||
);
|
||||
if (newPatterns.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const pattern of newPatterns) {
|
||||
ignorePatterns.add(pattern);
|
||||
}
|
||||
handleUpdate();
|
||||
},
|
||||
|
||||
setDisabled(value: boolean): void {
|
||||
if (value === disabled) {
|
||||
return;
|
||||
}
|
||||
disabled = value;
|
||||
handleUpdate();
|
||||
},
|
||||
|
||||
isDisabled(): boolean {
|
||||
return disabled;
|
||||
},
|
||||
|
||||
observe(observer: Observer): Subscription {
|
||||
const subscription = {observer};
|
||||
observers.add(subscription);
|
||||
observer(projection);
|
||||
return {
|
||||
unsubscribe(): void {
|
||||
observers.delete(subscription);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = YellowBoxRegistry;
|
75
Libraries/YellowBox/Data/YellowBoxSymbolication.js
Normal file
75
Libraries/YellowBox/Data/YellowBoxSymbolication.js
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const symbolicateStackTrace = require('symbolicateStackTrace');
|
||||
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
type CacheKey = string;
|
||||
|
||||
export type Stack = Array<StackFrame>;
|
||||
|
||||
const cache: Map<CacheKey, Promise<Stack>> = new Map();
|
||||
|
||||
const YellowBoxSymbolication = {
|
||||
symbolicate(stack: Stack): Promise<Stack> {
|
||||
const key = getCacheKey(stack);
|
||||
|
||||
let promise = cache.get(key);
|
||||
if (promise == null) {
|
||||
promise = symbolicateStackTrace(stack).then(sanitize);
|
||||
cache.set(key, promise);
|
||||
}
|
||||
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
|
||||
const getCacheKey = (stack: Stack): CacheKey => {
|
||||
return JSON.stringify(stack);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sanitize because sometimes, `symbolicateStackTrace` gives us invalid values.
|
||||
*/
|
||||
const sanitize = (maybeStack: mixed): Stack => {
|
||||
if (!Array.isArray(maybeStack)) {
|
||||
throw new Error('Expected stack to be an array.');
|
||||
}
|
||||
const stack = [];
|
||||
for (const maybeFrame of maybeStack) {
|
||||
if (typeof maybeFrame !== 'object' || maybeFrame == null) {
|
||||
throw new Error('Expected each stack frame to be an object.');
|
||||
}
|
||||
if (typeof maybeFrame.column !== 'number' && maybeFrame.column != null) {
|
||||
throw new Error('Expected stack frame `column` to be a nullable number.');
|
||||
}
|
||||
if (typeof maybeFrame.file !== 'string') {
|
||||
throw new Error('Expected stack frame `file` to be a string.');
|
||||
}
|
||||
if (typeof maybeFrame.lineNumber !== 'number') {
|
||||
throw new Error('Expected stack frame `lineNumber` to be a number.');
|
||||
}
|
||||
if (typeof maybeFrame.methodName !== 'string') {
|
||||
throw new Error('Expected stack frame `methodName` to be a string.');
|
||||
}
|
||||
stack.push({
|
||||
column: maybeFrame.column,
|
||||
file: maybeFrame.file,
|
||||
lineNumber: maybeFrame.lineNumber,
|
||||
methodName: maybeFrame.methodName,
|
||||
});
|
||||
}
|
||||
return stack;
|
||||
};
|
||||
|
||||
module.exports = YellowBoxSymbolication;
|
108
Libraries/YellowBox/Data/YellowBoxWarning.js
Normal file
108
Libraries/YellowBox/Data/YellowBoxWarning.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBoxCategory = require('YellowBoxCategory');
|
||||
const YellowBoxSymbolication = require('YellowBoxSymbolication');
|
||||
|
||||
const parseErrorStack = require('parseErrorStack');
|
||||
|
||||
import type {Category, Message} from 'YellowBoxCategory';
|
||||
import type {Stack} from 'YellowBoxSymbolication';
|
||||
|
||||
export type SymbolicationRequest = $ReadOnly<{|
|
||||
abort: () => void,
|
||||
|}>;
|
||||
|
||||
class YellowBoxWarning {
|
||||
static parse({
|
||||
args,
|
||||
framesToPop,
|
||||
}: $ReadOnly<{|
|
||||
args: $ReadOnlyArray<mixed>,
|
||||
framesToPop: number,
|
||||
|}>): {|
|
||||
category: Category,
|
||||
message: Message,
|
||||
stack: Stack,
|
||||
|} {
|
||||
return {
|
||||
...YellowBoxCategory.parse(args),
|
||||
stack: createStack({framesToPop: framesToPop + 1}),
|
||||
};
|
||||
}
|
||||
|
||||
message: Message;
|
||||
stack: Stack;
|
||||
symbolicated:
|
||||
| $ReadOnly<{|error: null, stack: null, status: 'NONE'|}>
|
||||
| $ReadOnly<{|error: null, stack: null, status: 'PENDING'|}>
|
||||
| $ReadOnly<{|error: null, stack: Stack, status: 'COMPLETE'|}>
|
||||
| $ReadOnly<{|error: Error, stack: null, status: 'FAILED'|}> = {
|
||||
error: null,
|
||||
stack: null,
|
||||
status: 'NONE',
|
||||
};
|
||||
|
||||
constructor(message: Message, stack: Stack) {
|
||||
this.message = message;
|
||||
this.stack = stack;
|
||||
}
|
||||
|
||||
getAvailableStack(): Stack {
|
||||
return this.symbolicated.status === 'COMPLETE'
|
||||
? this.symbolicated.stack
|
||||
: this.stack;
|
||||
}
|
||||
|
||||
symbolicate(callback: () => void): SymbolicationRequest {
|
||||
let aborted = false;
|
||||
|
||||
if (this.symbolicated.status !== 'COMPLETE') {
|
||||
const updateStatus = (error: ?Error, stack: ?Stack): void => {
|
||||
if (error != null) {
|
||||
this.symbolicated = {error, stack: null, status: 'FAILED'};
|
||||
} else if (stack != null) {
|
||||
this.symbolicated = {error: null, stack, status: 'COMPLETE'};
|
||||
} else {
|
||||
this.symbolicated = {error: null, stack: null, status: 'PENDING'};
|
||||
}
|
||||
if (!aborted) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus(null, null);
|
||||
YellowBoxSymbolication.symbolicate(this.stack).then(
|
||||
stack => {
|
||||
updateStatus(null, stack);
|
||||
},
|
||||
error => {
|
||||
updateStatus(error, null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
abort(): void {
|
||||
aborted = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createStack({framesToPop}: $ReadOnly<{|framesToPop: number|}>): Stack {
|
||||
const error: any = new Error();
|
||||
error.framesToPop = framesToPop + 1;
|
||||
return parseErrorStack(error);
|
||||
}
|
||||
|
||||
module.exports = YellowBoxWarning;
|
100
Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js
Normal file
100
Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBoxCategory = require('YellowBoxCategory');
|
||||
|
||||
describe('YellowBoxCategory', () => {
|
||||
it('parses strings', () => {
|
||||
expect(YellowBoxCategory.parse(['A'])).toEqual({
|
||||
category: 'A',
|
||||
message: {
|
||||
content: 'A',
|
||||
substitutions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses strings with arguments', () => {
|
||||
expect(YellowBoxCategory.parse(['A', 'B', 'C'])).toEqual({
|
||||
category: 'A "B" "C"',
|
||||
message: {
|
||||
content: 'A "B" "C"',
|
||||
substitutions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses formatted strings', () => {
|
||||
expect(YellowBoxCategory.parse(['%s', 'A'])).toEqual({
|
||||
category: '\ufeff%s',
|
||||
message: {
|
||||
content: '"A"',
|
||||
substitutions: [
|
||||
{
|
||||
length: 3,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses formatted strings with insufficient arguments', () => {
|
||||
expect(YellowBoxCategory.parse(['%s %s', 'A'])).toEqual({
|
||||
category: '\ufeff%s %s',
|
||||
message: {
|
||||
content: '"A" %s',
|
||||
substitutions: [
|
||||
{
|
||||
length: 3,
|
||||
offset: 0,
|
||||
},
|
||||
{
|
||||
length: 2,
|
||||
offset: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses formatted strings with excess arguments', () => {
|
||||
expect(YellowBoxCategory.parse(['%s', 'A', 'B'])).toEqual({
|
||||
category: '\ufeff%s "B"',
|
||||
message: {
|
||||
content: '"A" "B"',
|
||||
substitutions: [
|
||||
{
|
||||
length: 3,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('treats "%s" in arguments as literals', () => {
|
||||
expect(YellowBoxCategory.parse(['%s', '%s', 'A'])).toEqual({
|
||||
category: '\ufeff%s "A"',
|
||||
message: {
|
||||
content: '"%s" "A"',
|
||||
substitutions: [
|
||||
{
|
||||
length: 4,
|
||||
offset: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
267
Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js
Normal file
267
Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js
Normal file
@ -0,0 +1,267 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBoxCategory = require('YellowBoxCategory');
|
||||
const YellowBoxRegistry = require('YellowBoxRegistry');
|
||||
|
||||
const registry = () => {
|
||||
const observer = jest.fn();
|
||||
YellowBoxRegistry.observe(observer).unsubscribe();
|
||||
return observer.mock.calls[0][0];
|
||||
};
|
||||
|
||||
const observe = () => {
|
||||
const observer = jest.fn();
|
||||
return {
|
||||
observer,
|
||||
subscription: YellowBoxRegistry.observe(observer),
|
||||
};
|
||||
};
|
||||
|
||||
describe('YellowBoxRegistry', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('adds and deletes warnings', () => {
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
const {category: categoryA} = YellowBoxCategory.parse(['A']);
|
||||
|
||||
expect(registry().size).toBe(1);
|
||||
expect(registry().get(categoryA)).not.toBe(undefined);
|
||||
|
||||
YellowBoxRegistry.delete(categoryA);
|
||||
expect(registry().size).toBe(0);
|
||||
expect(registry().get(categoryA)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('clears all warnings', () => {
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['C'], framesToPop: 0});
|
||||
|
||||
expect(registry().size).toBe(3);
|
||||
|
||||
YellowBoxRegistry.clear();
|
||||
expect(registry().size).toBe(0);
|
||||
});
|
||||
|
||||
it('sorts warnings in chronological order', () => {
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['C'], framesToPop: 0});
|
||||
|
||||
const {category: categoryA} = YellowBoxCategory.parse(['A']);
|
||||
const {category: categoryB} = YellowBoxCategory.parse(['B']);
|
||||
const {category: categoryC} = YellowBoxCategory.parse(['C']);
|
||||
|
||||
expect(Array.from(registry().keys())).toEqual([
|
||||
categoryA,
|
||||
categoryB,
|
||||
categoryC,
|
||||
]);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
|
||||
// Expect `A` to be hoisted to the end of the registry.
|
||||
expect(Array.from(registry().keys())).toEqual([
|
||||
categoryB,
|
||||
categoryC,
|
||||
categoryA,
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores warnings matching patterns', () => {
|
||||
YellowBoxRegistry.add({args: ['A!'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['B?'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['C!'], framesToPop: 0});
|
||||
expect(registry().size).toBe(3);
|
||||
|
||||
YellowBoxRegistry.addIgnorePatterns(['!']);
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.addIgnorePatterns(['?']);
|
||||
expect(registry().size).toBe(0);
|
||||
});
|
||||
|
||||
it('ignores all warnings when disabled', () => {
|
||||
YellowBoxRegistry.add({args: ['A!'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['B?'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['C!'], framesToPop: 0});
|
||||
expect(registry().size).toBe(3);
|
||||
|
||||
YellowBoxRegistry.setDisabled(true);
|
||||
expect(registry().size).toBe(0);
|
||||
|
||||
YellowBoxRegistry.setDisabled(false);
|
||||
expect(registry().size).toBe(3);
|
||||
});
|
||||
|
||||
it('groups warnings by simple categories', () => {
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(2);
|
||||
});
|
||||
|
||||
it('groups warnings by format string categories', () => {
|
||||
YellowBoxRegistry.add({args: ['%s', 'A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['%s', 'B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(2);
|
||||
|
||||
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(3);
|
||||
});
|
||||
|
||||
it('groups warnings with consideration for arguments', () => {
|
||||
YellowBoxRegistry.add({args: ['A', 'B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A', 'B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A', 'C'], framesToPop: 0});
|
||||
expect(registry().size).toBe(2);
|
||||
|
||||
YellowBoxRegistry.add({args: ['%s', 'A', 'A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(3);
|
||||
|
||||
YellowBoxRegistry.add({args: ['%s', 'B', 'A'], framesToPop: 0});
|
||||
expect(registry().size).toBe(3);
|
||||
|
||||
YellowBoxRegistry.add({args: ['%s', 'B', 'B'], framesToPop: 0});
|
||||
expect(registry().size).toBe(4);
|
||||
});
|
||||
|
||||
it('ignores warnings starting with "(ADVICE)"', () => {
|
||||
YellowBoxRegistry.add({args: ['(ADVICE) ...'], framesToPop: 0});
|
||||
expect(registry().size).toBe(0);
|
||||
});
|
||||
|
||||
it('does not ignore warnings formatted to start with "(ADVICE)"', () => {
|
||||
YellowBoxRegistry.add({args: ['%s ...', '(ADVICE)'], framesToPop: 0});
|
||||
expect(registry().size).toBe(1);
|
||||
});
|
||||
|
||||
it('immediately updates new observers', () => {
|
||||
const {observer} = observe();
|
||||
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
expect(observer.mock.calls[0][0]).toBe(registry());
|
||||
});
|
||||
|
||||
it('sends batched updates asynchoronously', () => {
|
||||
const {observer} = observe();
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
YellowBoxRegistry.add({args: ['B'], framesToPop: 0});
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('stops sending updates to unsubscribed observers', () => {
|
||||
const {observer, subscription} = observe();
|
||||
subscription.unsubscribe();
|
||||
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
expect(observer.mock.calls[0][0]).toBe(registry());
|
||||
});
|
||||
|
||||
it('updates observers when a warning is added or deleted', () => {
|
||||
const {observer} = observe();
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
|
||||
const {category: categoryA} = YellowBoxCategory.parse(['A']);
|
||||
YellowBoxRegistry.delete(categoryA);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
|
||||
// Does nothing when category does not exist.
|
||||
YellowBoxRegistry.delete(categoryA);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('updates observers when cleared', () => {
|
||||
const {observer} = observe();
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
|
||||
YellowBoxRegistry.add({args: ['A'], framesToPop: 0});
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
|
||||
YellowBoxRegistry.clear();
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
|
||||
// Does nothing when already empty.
|
||||
YellowBoxRegistry.clear();
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('updates observers when an ignore pattern is added', () => {
|
||||
const {observer} = observe();
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
|
||||
YellowBoxRegistry.addIgnorePatterns(['?']);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
|
||||
YellowBoxRegistry.addIgnorePatterns(['!']);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
|
||||
// Does nothing for an existing ignore pattern.
|
||||
YellowBoxRegistry.addIgnorePatterns(['!']);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('updates observers when disabled or enabled', () => {
|
||||
const {observer} = observe();
|
||||
expect(observer.mock.calls.length).toBe(1);
|
||||
|
||||
YellowBoxRegistry.setDisabled(true);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
|
||||
// Does nothing when already disabled.
|
||||
YellowBoxRegistry.setDisabled(true);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(2);
|
||||
|
||||
YellowBoxRegistry.setDisabled(false);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
|
||||
// Does nothing when already enabled.
|
||||
YellowBoxRegistry.setDisabled(false);
|
||||
jest.runAllImmediates();
|
||||
expect(observer.mock.calls.length).toBe(3);
|
||||
});
|
||||
});
|
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
jest.mock('symbolicateStackTrace');
|
||||
|
||||
const YellowBoxSymbolication = require('YellowBoxSymbolication');
|
||||
|
||||
const symbolicateStackTrace: JestMockFn<
|
||||
$ReadOnlyArray<Array<StackFrame>>,
|
||||
Promise<Array<StackFrame>>,
|
||||
> = (require('symbolicateStackTrace'): any);
|
||||
|
||||
const createStack = methodNames =>
|
||||
methodNames.map(methodName => ({
|
||||
column: null,
|
||||
file: 'file://path/to/file.js',
|
||||
lineNumber: 1,
|
||||
methodName,
|
||||
}));
|
||||
|
||||
describe('YellowBoxSymbolication', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
symbolicateStackTrace.mockImplementation(async stack => stack);
|
||||
});
|
||||
|
||||
it('symbolicates different stacks', () => {
|
||||
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
|
||||
YellowBoxSymbolication.symbolicate(createStack(['D', 'E', 'F']));
|
||||
|
||||
expect(symbolicateStackTrace.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('batch symbolicates equivalent stacks', () => {
|
||||
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
|
||||
YellowBoxSymbolication.symbolicate(createStack(['A', 'B', 'C']));
|
||||
|
||||
expect(symbolicateStackTrace.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
126
Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js
Normal file
126
Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js
Normal file
@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
jest.mock('YellowBoxSymbolication');
|
||||
|
||||
const YellowBoxSymbolication: {|
|
||||
symbolicate: JestMockFn<
|
||||
$ReadOnlyArray<Array<StackFrame>>,
|
||||
Promise<Array<StackFrame>>,
|
||||
>,
|
||||
|} = (require('YellowBoxSymbolication'): any);
|
||||
const YellowBoxWarning = require('YellowBoxWarning');
|
||||
|
||||
const createStack = methodNames =>
|
||||
methodNames.map(methodName => ({
|
||||
column: null,
|
||||
file: 'file://path/to/file.js',
|
||||
lineNumber: 1,
|
||||
methodName,
|
||||
}));
|
||||
|
||||
describe('YellowBoxWarning', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
YellowBoxSymbolication.symbolicate.mockImplementation(async stack =>
|
||||
createStack(stack.map(frame => `S(${frame.methodName})`)),
|
||||
);
|
||||
});
|
||||
|
||||
it('starts without a symbolicated stack', () => {
|
||||
const warning = new YellowBoxWarning(
|
||||
{content: '...', substitutions: []},
|
||||
createStack(['A', 'B', 'C']),
|
||||
);
|
||||
|
||||
expect(warning.symbolicated).toEqual({
|
||||
error: null,
|
||||
stack: null,
|
||||
status: 'NONE',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates when symbolication is in progress', () => {
|
||||
const warning = new YellowBoxWarning(
|
||||
{content: '...', substitutions: []},
|
||||
createStack(['A', 'B', 'C']),
|
||||
);
|
||||
const callback = jest.fn();
|
||||
warning.symbolicate(callback);
|
||||
|
||||
expect(callback.mock.calls.length).toBe(1);
|
||||
expect(warning.symbolicated).toEqual({
|
||||
error: null,
|
||||
stack: null,
|
||||
status: 'PENDING',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates when symbolication finishes', () => {
|
||||
const warning = new YellowBoxWarning(
|
||||
{content: '...', substitutions: []},
|
||||
createStack(['A', 'B', 'C']),
|
||||
);
|
||||
const callback = jest.fn();
|
||||
warning.symbolicate(callback);
|
||||
|
||||
jest.runAllTicks();
|
||||
|
||||
expect(callback.mock.calls.length).toBe(2);
|
||||
expect(warning.symbolicated).toEqual({
|
||||
error: null,
|
||||
stack: createStack(['S(A)', 'S(B)', 'S(C)']),
|
||||
status: 'COMPLETE',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates when symbolication fails', () => {
|
||||
const error = new Error('...');
|
||||
YellowBoxSymbolication.symbolicate.mockImplementation(async stack => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const warning = new YellowBoxWarning(
|
||||
{content: '...', substitutions: []},
|
||||
createStack(['A', 'B', 'C']),
|
||||
);
|
||||
const callback = jest.fn();
|
||||
warning.symbolicate(callback);
|
||||
|
||||
jest.runAllTicks();
|
||||
|
||||
expect(callback.mock.calls.length).toBe(2);
|
||||
expect(warning.symbolicated).toEqual({
|
||||
error,
|
||||
stack: null,
|
||||
status: 'FAILED',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update aborted requests', () => {
|
||||
const warning = new YellowBoxWarning(
|
||||
{content: '...', substitutions: []},
|
||||
createStack(['A', 'B', 'C']),
|
||||
);
|
||||
const callback = jest.fn();
|
||||
const request = warning.symbolicate(callback);
|
||||
request.abort();
|
||||
|
||||
jest.runAllTicks();
|
||||
|
||||
expect(callback.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
53
Libraries/YellowBox/UI/YellowBoxButton.js
Normal file
53
Libraries/YellowBox/UI/YellowBoxButton.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const YellowBoxPressable = require('YellowBoxPressable');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
hitSlop?: ?EdgeInsetsProp,
|
||||
label: string,
|
||||
onPress: () => void,
|
||||
|}>;
|
||||
|
||||
const YellowBoxButton = (props: Props): React.Node => (
|
||||
<YellowBoxPressable
|
||||
hitSlop={props.hitSlop}
|
||||
onPress={props.onPress}
|
||||
style={styles.root}>
|
||||
<Text numberOfLines={1} style={styles.label}>
|
||||
{props.label}
|
||||
</Text>
|
||||
</YellowBoxPressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
borderRadius: 14,
|
||||
height: 28,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
label: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 12,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxButton;
|
52
Libraries/YellowBox/UI/YellowBoxImageSource.js
Normal file
52
Libraries/YellowBox/UI/YellowBoxImageSource.js
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const PixelRatio = require('PixelRatio');
|
||||
|
||||
const scale = PixelRatio.get();
|
||||
|
||||
/**
|
||||
* We use inline images for YellowBox in order to avoid display latency due to
|
||||
* resource contention with symbolicating stack traces.
|
||||
*
|
||||
* The following steps were used to create these:
|
||||
*
|
||||
* 1. Download SVG files from: https://feathericons.com
|
||||
* 2. Rasterize SVG files to PNG files at 16dp, 36dp, and 48dp.
|
||||
* 3. Convert to Base64: https://www.google.com/search?q=base64+image+encoder
|
||||
*
|
||||
* @see https://github.com/feathericons/feather
|
||||
* @copyright 2013-2017 Cole Bemis
|
||||
* @license MIT
|
||||
*/
|
||||
const YellowBoxImageSource = {
|
||||
alertTriangle:
|
||||
scale > 2
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAB60lEQVRoge2Z3W3DIBSFj9oFPAIjZARGyAiMkBHuJh4hI2QEj5AR3Me+tQ91JALHmD8bKvmTkCr5Auc6/kzUACcnRXzuvL4GoAB8Afjeea9qXADcAfw4475c65orgBl++NeYl5ouUQiHt5tQTRJuwB6b5zLY49QVGn7I0bo+kuv60IQbuHf5CWCIqOkCgX93maia1MkRAUMo+OI+AvUPp7a50EzcUCBF6psJrUkYiZgnZJ7eId8mMeIyhpW5hyLw72LKCXsl86VqwgAKceKapW5e/nZpJnSsuHaTM7muyDq7C63JprJS69YxhNTpSlkpKeLGNHCo0EJChcSNaQA4SGiFtBMXJFSI3YVOPXFB6kMoUl9NaE0Wl4h5KQ0AOwqde+KmNrCL0EKCxJ64qQ0AlYVWSBfXZusgW6Oa0Dni2hiEv0qsoci+yUJrsoikLlKAkP11ygK54taiSOgb/O5b/DMqS+gBZeLWJlnoEX7XwQkBDPIktlEkz7hWrEmxZG4M5L9GXYTk0qxwcopKxa3VABN6cosM/C5LxTUof4ReMKHf1nRlaSnuGsGM7kfU4w8RF5Bz4aNlokLe/HQ/ngl9/Qih4L9k3h4hA1+S3odxu3Q77Hl4r1Hg75n6D01M2Difbp02Mi3ZTk5OLH4BUyEtOlDYuK0AAAAASUVORK5CYII='
|
||||
: scale > 1
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABVklEQVRYheWX4U3DMBBGH4gBMoJHyAgeoSNkAxjBG5QNOkJHCGzQDcoGZQP4gY3Oqe1cEscS4pNOqs9Jvqvv6ZrCf9fDhnutD4A3H810Br4mcW5l7hLmIdze5mZi+OJD5syeBYzC6CjyR5Ef9zI/CJMb0Im9zufC/qG2eQdchcGQuGYQ+9dJgZvl0B2xbJGrZW6IIevFXp9YVwcyB540syJfFcgSeJb0cVcDcg68XAFQCUhH+ShLBcBGIA158LQFqIB8zBRwEp9fgctcxQld/L2pZxZVAk/KkucjaDGQmoknrz35KEE2sABIRxm8tVIBaZgHb61UQOYmXk7aFgQVJ6QWPCnLAriYAVILnpTxD7yh/9EZiIEE4m+y29uMkGy1nQ6i9wYFRB5PwKdYP/v1msmnUe89gn695bG0iqjdXeMiRu9599csvGKZ0jlu0Ac/7d2rxX9Q37HW6QfX/ZguAAAAAElFTkSuQmCC'
|
||||
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAvUlEQVQ4jbWT4Q3CIBCFP40DdANxg24gIzhKuwEjuIFxAkcwTtARGicoG+iPXlMCB8UfvoQc4e7ePV4A/ogWuMlqc0W7AsEo0QMNcPplugMmwMia5KwKWkNIuIkHq3wLXGQ/Sq4IC3wkLpOfmZyKeEpIEKsDYB8VN0Afkfpg30uNiycbdKcNqXEOxdBEWoEAoqta8uZ0iqqkxwGDUrSFAXAHZpOWd/+ubD5Kz335Cx1wZna4Bh54AddauVl8ARfCLO9Xq7xGAAAAAElFTkSuQmCC',
|
||||
check:
|
||||
scale > 2
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAqElEQVRoge3YTQ7CIBRF4bPUu/8JS6gTSaqilh95vuR+CaO2cGgYNAUzMzOzFgHlPhRaMkDAcRoltKaTeIxPtQHxGn+Q5AgJx8cQjo8hHB9DOP76Yiu/RcTmN18WLiQCjs3zBkYXVGOeLWd+xcIr5pgyEzDz7FIjISPP/FRPUM+9W4nvYVfuCSXeB3669ldEOzRFfCUSx1cicXwlEsdXIvEPKDMzM7PMbtugw3XTpNA2AAAAAElFTkSuQmCC'
|
||||
: scale > 1
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAf0lEQVRYhe2UvQ2AIBQGL3EBR3AESkv3bxxFN8DmWUgwvkI+En1X0cBd+IMg+DuDyDMCs413kfMiX4EMbD3l8oCaPIU85B4mYLEF5XJscrYFPRGvb/sZ4IlocubJGdH0wj1FSG77XYT0qdUi5O+8jOjyyZQRUnkZ0UUeBMF3OQC/0VsyGlxligAAAABJRU5ErkJggg=='
|
||||
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAASElEQVQ4jWNgGJHAgIGBIYESze8ZGBjWU6L5PAMDgwBNNCdAFZJt83qoQmRDSHK2AFQhzBCy/IxsCNkBJsDAwLAfiknWPBIBAETPFeuA4fr6AAAAAElFTkSuQmCC',
|
||||
loader:
|
||||
scale > 2
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABXElEQVRoge2a3W3DMAyEr+0CHkGjaISOcKN4k6zQETpCR+gGzgbpQ10kcamIpKQ6avQBBPxg3pHwL2UDg/8LASxrcNdKnCwATmssrUyeWgnju/DmXs8tRP+Sh2kgAJga1rFlWj2rcMD5YqQh77QJLbzIORjyRIJQCJW5ngYo5AVlrsgkCGqbsDbAhFfxqZsSZibP0oDXQ43HQPsg82i7sBoR+VcJq2YxKcPo0IoJLRZXmYGC6ezQmQUdVqhPBVH/CNBTSMkLVlzjA8Bbocb7GoPBoADi+umZilYzbrG/JrnljOvy734iu4To/BQaDB6Rl4LciPPF9Lmjhgvi+s7w6tCIGw3WKS0P8fvWNjt0ZkGHFeq7CQXTbkZKGg2JOxrqPUZ3s6ziNdju38IjS/dLi0EQpDLX2gDQYHEX6Hx5/YcA+6H0NgAYPnCMj3x7Mxq4wTGx3Q1E578aDDR8AX0mOGD6BEN/AAAAAElFTkSuQmCC'
|
||||
: scale > 1
|
||||
? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN0lEQVRYhe2WzU3EMBCFP34KyJEjJaQDXAIlJJ24BSow2wEdhHSwJSwd7JHbcmC0mOxMnDiWDIInWbHkN29exo4n8IvRAEFGU8OAA04yulyR60Jm7msbyIZloAMGwBfI4UWrWxM08LW/weC4iOMNTog4g0awKjBG827GxBwC3996NHizAifsSrTRmlsZm23CT9adktyXSq6ZUPdxgiXnZzW8CLcLuC3lvqA/gCt5NtjlPQL7TP0Wu1HtRRu4PO3T4TKTz2kG+AG9IN6CR/Su9iojBw69egfghWgL/pGCp+JFVPUqTjWjlsuqeAo1o6rt2C8QcNiV0UxoHPMieojmz0CfMKyhl1hN84xbI3gnz5Ftp7kH3iT5LsFdDUf6pzSJ6r2glIFDbuDNhqRH4I7Pvv4EvG/QqocP2Jh/xzzX/zUAAAAASUVORK5CYII='
|
||||
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAsklEQVQ4jaWTYRHCMAyFP7gJmIQ6oChgEpBQKXMwC3MADpAAEiphDuBHC4QuDRu8u9ylyWtem7Rgw2X7GT1wsghb4beAVzhtsfYyJgs44AoEQzBkjrMId1HkKPwyZ6oMSnxYsnk1NqT7yMo34Fzhd9meGJvs7Hh3NhqCLXDI/rT0lKsR+KOJgc9RdaRRarkZvELogYsi8HqxjUhGYE+aQg1jzketwFTZXHbbEpjB8eU7PwAbLiJz46707gAAAABJRU5ErkJggg==',
|
||||
};
|
||||
|
||||
module.exports = YellowBoxImageSource;
|
190
Libraries/YellowBox/UI/YellowBoxInspector.js
Normal file
190
Libraries/YellowBox/UI/YellowBoxInspector.js
Normal file
@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Platform = require('Platform');
|
||||
const React = require('React');
|
||||
const ScrollView = require('ScrollView');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const View = require('View');
|
||||
const YellowBoxCategory = require('YellowBoxCategory');
|
||||
const YellowBoxInspectorFooter = require('YellowBoxInspectorFooter');
|
||||
const YellowBoxInspectorHeader = require('YellowBoxInspectorHeader');
|
||||
const YellowBoxInspectorSourceMapStatus = require('YellowBoxInspectorSourceMapStatus');
|
||||
const YellowBoxInspectorStackFrame = require('YellowBoxInspectorStackFrame');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
const openFileInEditor = require('openFileInEditor');
|
||||
|
||||
import type YellowBoxWarning from 'YellowBoxWarning';
|
||||
import type {SymbolicationRequest} from 'YellowBoxWarning';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
onDismiss: () => void,
|
||||
onMinimize: () => void,
|
||||
warnings: $ReadOnlyArray<YellowBoxWarning>,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
selectedIndex: number,
|
||||
|};
|
||||
|
||||
class YellowBoxInspector extends React.Component<Props, State> {
|
||||
_symbolication: ?SymbolicationRequest;
|
||||
|
||||
state = {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {warnings} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
|
||||
const warning = warnings[selectedIndex];
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<YellowBoxInspectorHeader
|
||||
onSelectIndex={this._handleSelectIndex}
|
||||
selectedIndex={selectedIndex}
|
||||
warnings={warnings}
|
||||
/>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.bodyContent}
|
||||
key={selectedIndex}
|
||||
style={styles.body}>
|
||||
<View>
|
||||
<View style={styles.bodyHeading}>
|
||||
<Text style={styles.bodyHeadingText}>Warning</Text>
|
||||
</View>
|
||||
<Text style={styles.bodyText}>
|
||||
{YellowBoxCategory.render(
|
||||
warning.message,
|
||||
styles.substitutionText,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.bodySection}>
|
||||
<View style={styles.bodyHeading}>
|
||||
<Text style={styles.bodyHeadingText}>Stack</Text>
|
||||
<YellowBoxInspectorSourceMapStatus
|
||||
status={warning.symbolicated.status}
|
||||
/>
|
||||
</View>
|
||||
{warning.getAvailableStack().map((frame, index) => (
|
||||
<YellowBoxInspectorStackFrame
|
||||
key={index}
|
||||
frame={frame}
|
||||
onPress={
|
||||
warning.symbolicated.status === 'COMPLETE'
|
||||
? () => {
|
||||
openFileInEditor(frame.file, frame.lineNumber);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<YellowBoxInspectorFooter
|
||||
onDismiss={this.props.onDismiss}
|
||||
onMinimize={this.props.onMinimize}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this._handleSymbolication();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State): void {
|
||||
if (
|
||||
prevProps.warnings !== this.props.warnings ||
|
||||
prevState.selectedIndex !== this.state.selectedIndex
|
||||
) {
|
||||
this._cancelSymbolication();
|
||||
this._handleSymbolication();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this._cancelSymbolication();
|
||||
}
|
||||
|
||||
_handleSymbolication(): void {
|
||||
const warning = this.props.warnings[this.state.selectedIndex];
|
||||
if (warning.symbolicated.status !== 'COMPLETE') {
|
||||
this._symbolication = warning.symbolicate(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_cancelSymbolication(): void {
|
||||
if (this._symbolication != null) {
|
||||
this._symbolication.abort();
|
||||
this._symbolication = null;
|
||||
}
|
||||
}
|
||||
|
||||
_handleSelectIndex = (selectedIndex: number): void => {
|
||||
this.setState({selectedIndex});
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
elevation: Platform.OS === 'android' ? Number.MAX_SAFE_INTEGER : undefined,
|
||||
height: '100%',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: YellowBoxStyle.getBackgroundColor(0.95),
|
||||
borderBottomColor: YellowBoxStyle.getDividerColor(0.95),
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: YellowBoxStyle.getDividerColor(0.95),
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
flex: 1,
|
||||
},
|
||||
bodyContent: {
|
||||
paddingVertical: 12,
|
||||
},
|
||||
bodyHeading: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 6,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
bodyHeadingText: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
flex: 1,
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
includeFontPadding: false,
|
||||
lineHeight: 28,
|
||||
},
|
||||
bodyText: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 14,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 18,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
substitutionText: {
|
||||
color: YellowBoxStyle.getTextColor(0.6),
|
||||
},
|
||||
bodySection: {
|
||||
marginTop: 20,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxInspector;
|
76
Libraries/YellowBox/UI/YellowBoxInspectorFooter.js
Normal file
76
Libraries/YellowBox/UI/YellowBoxInspectorFooter.js
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const SafeAreaView = require('SafeAreaView');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const View = require('View');
|
||||
const YellowBoxPressable = require('YellowBoxPressable');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
onDismiss: () => void,
|
||||
onMinimize: () => void,
|
||||
|}>;
|
||||
|
||||
const YellowBoxInspectorFooter = (props: Props): React.Node => (
|
||||
<View style={styles.root}>
|
||||
<YellowBoxPressable
|
||||
backgroundColor={{
|
||||
default: 'transparent',
|
||||
pressed: YellowBoxStyle.getHighlightColor(1),
|
||||
}}
|
||||
onPress={props.onMinimize}
|
||||
style={styles.button}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.label}>Minimize</Text>
|
||||
</View>
|
||||
<SafeAreaView />
|
||||
</YellowBoxPressable>
|
||||
<YellowBoxPressable
|
||||
backgroundColor={{
|
||||
default: 'transparent',
|
||||
pressed: YellowBoxStyle.getHighlightColor(1),
|
||||
}}
|
||||
onPress={props.onDismiss}
|
||||
style={styles.button}>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.label}>Dismiss</Text>
|
||||
</View>
|
||||
<SafeAreaView />
|
||||
</YellowBoxPressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
backgroundColor: YellowBoxStyle.getBackgroundColor(0.95),
|
||||
flexDirection: 'row',
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
label: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 14,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 18,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxInspectorFooter;
|
108
Libraries/YellowBox/UI/YellowBoxInspectorHeader.js
Normal file
108
Libraries/YellowBox/UI/YellowBoxInspectorHeader.js
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Platform = require('Platform');
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const UTFSequence = require('UTFSequence');
|
||||
const View = require('View');
|
||||
const YellowBoxPressable = require('YellowBoxPressable');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type YellowBoxWarning from 'YellowBoxWarning';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
onSelectIndex: (selectedIndex: number) => void,
|
||||
selectedIndex: number,
|
||||
warnings: $ReadOnlyArray<YellowBoxWarning>,
|
||||
|}>;
|
||||
|
||||
const YellowBoxInspectorHeader = (props: Props): React.Node => {
|
||||
const prevIndex = props.selectedIndex - 1;
|
||||
const nextIndex = props.selectedIndex + 1;
|
||||
|
||||
const titleText =
|
||||
props.warnings.length === 1
|
||||
? 'Single Occurrence'
|
||||
: `Occurrence ${props.selectedIndex + 1} of ${props.warnings.length}`;
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<YellowBoxInspectorHeaderButton
|
||||
disabled={props.warnings[prevIndex] == null}
|
||||
label={UTFSequence.TRIANGLE_LEFT}
|
||||
onPress={() => props.onSelectIndex(prevIndex)}
|
||||
/>
|
||||
<View style={styles.headerTitle}>
|
||||
<Text style={styles.headerTitleText}>{titleText}</Text>
|
||||
</View>
|
||||
<YellowBoxInspectorHeaderButton
|
||||
disabled={props.warnings[nextIndex] == null}
|
||||
label={UTFSequence.TRIANGLE_RIGHT}
|
||||
onPress={() => props.onSelectIndex(nextIndex)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const YellowBoxInspectorHeaderButton = (
|
||||
props: $ReadOnly<{|
|
||||
disabled: boolean,
|
||||
label: React.Node,
|
||||
onPress?: ?() => void,
|
||||
|}>,
|
||||
): React.Node => (
|
||||
<YellowBoxPressable
|
||||
onPress={props.disabled ? null : props.onPress}
|
||||
style={styles.headerButton}>
|
||||
{props.disabled ? null : (
|
||||
<Text style={styles.headerButtonText}>{props.label}</Text>
|
||||
)}
|
||||
</YellowBoxPressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
height: Platform.select({
|
||||
android: 48,
|
||||
ios: 44,
|
||||
}),
|
||||
},
|
||||
headerButton: {
|
||||
alignItems: 'center',
|
||||
aspectRatio: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerButtonText: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 16,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 20,
|
||||
},
|
||||
headerTitle: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: YellowBoxStyle.getBackgroundColor(0.95),
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
headerTitleText: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
includeFontPadding: false,
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxInspectorHeader;
|
149
Libraries/YellowBox/UI/YellowBoxInspectorSourceMapStatus.js
Normal file
149
Libraries/YellowBox/UI/YellowBoxInspectorSourceMapStatus.js
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Animated = require('Animated');
|
||||
const Easing = require('Easing');
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const View = require('View');
|
||||
const YellowBoxImageSource = require('YellowBoxImageSource');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type {CompositeAnimation} from 'AnimatedImplementation';
|
||||
import type AnimatedInterpolation from 'AnimatedInterpolation';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
status: 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
animation: ?CompositeAnimation,
|
||||
rotate: ?AnimatedInterpolation,
|
||||
|};
|
||||
|
||||
class YellowBoxInspectorSourceMapStatus extends React.Component<Props, State> {
|
||||
state = {
|
||||
animation: null,
|
||||
rotate: null,
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
let image;
|
||||
switch (this.props.status) {
|
||||
case 'COMPLETE':
|
||||
image = YellowBoxImageSource.check;
|
||||
break;
|
||||
case 'FAILED':
|
||||
image = YellowBoxImageSource.alertTriangle;
|
||||
break;
|
||||
case 'PENDING':
|
||||
image = YellowBoxImageSource.loader;
|
||||
break;
|
||||
}
|
||||
|
||||
return image == null ? null : (
|
||||
<View
|
||||
style={StyleSheet.compose(
|
||||
styles.root,
|
||||
this.props.status === 'PENDING' ? styles.pending : null,
|
||||
)}>
|
||||
<Animated.Image
|
||||
source={{height: 16, uri: image, width: 16}}
|
||||
style={StyleSheet.compose(
|
||||
styles.image,
|
||||
this.state.rotate == null
|
||||
? null
|
||||
: {transform: [{rotate: this.state.rotate}]},
|
||||
)}
|
||||
/>
|
||||
<Text style={styles.text}>Source Map</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this._updateAnimation();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this._updateAnimation();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.state.animation != null) {
|
||||
this.state.animation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
_updateAnimation(): void {
|
||||
if (this.props.status === 'PENDING') {
|
||||
if (this.state.animation == null) {
|
||||
const animated = new Animated.Value(0);
|
||||
const animation = Animated.loop(
|
||||
Animated.timing(animated, {
|
||||
duration: 2000,
|
||||
easing: Easing.linear,
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
);
|
||||
this.setState(
|
||||
{
|
||||
animation,
|
||||
rotate: animated.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
}),
|
||||
},
|
||||
() => {
|
||||
animation.start();
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.state.animation != null) {
|
||||
this.state.animation.stop();
|
||||
this.setState({
|
||||
animation: null,
|
||||
rotate: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: YellowBoxStyle.getTextColor(0.8),
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
height: 24,
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
pending: {
|
||||
backgroundColor: YellowBoxStyle.getTextColor(0.6),
|
||||
},
|
||||
image: {
|
||||
marginEnd: 4,
|
||||
tintColor: YellowBoxStyle.getBackgroundColor(1),
|
||||
},
|
||||
text: {
|
||||
color: YellowBoxStyle.getBackgroundColor(1),
|
||||
fontSize: 12,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxInspectorSourceMapStatus;
|
81
Libraries/YellowBox/UI/YellowBoxInspectorStackFrame.js
Normal file
81
Libraries/YellowBox/UI/YellowBoxInspectorStackFrame.js
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const YellowBoxPressable = require('YellowBoxPressable');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type {PressEvent} from 'CoreEventTypes';
|
||||
import type {StackFrame} from 'parseErrorStack';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
frame: StackFrame,
|
||||
onPress?: ?(event: PressEvent) => void,
|
||||
|}>;
|
||||
|
||||
const YellowBoxInspectorStackFrame = (props: Props): React.Node => {
|
||||
const {frame, onPress} = props;
|
||||
|
||||
return (
|
||||
<YellowBoxPressable
|
||||
backgroundColor={{
|
||||
default: YellowBoxStyle.getBackgroundColor(0),
|
||||
pressed: YellowBoxStyle.getHighlightColor(1),
|
||||
}}
|
||||
onPress={onPress}
|
||||
style={styles.frame}>
|
||||
<Text style={styles.frameName}>{frame.methodName}</Text>
|
||||
<Text
|
||||
ellipsizeMode="middle"
|
||||
numberOfLines={1}
|
||||
style={styles.frameLocation}>
|
||||
{`${getFrameLocation(frame.file)}:${frame.lineNumber}${
|
||||
frame.column == null ? '' : ':' + frame.column
|
||||
}`}
|
||||
</Text>
|
||||
</YellowBoxPressable>
|
||||
);
|
||||
};
|
||||
|
||||
const getFrameLocation = (uri: string): string => {
|
||||
const queryIndex = uri.indexOf('?');
|
||||
const query = queryIndex < 0 ? '' : uri.substr(queryIndex);
|
||||
|
||||
const path = queryIndex < 0 ? uri : uri.substr(0, queryIndex);
|
||||
const file = path.substr(path.lastIndexOf('/') + 1);
|
||||
|
||||
return file + query;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
frame: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
frameName: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
fontSize: 14,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 18,
|
||||
},
|
||||
frameLocation: {
|
||||
color: YellowBoxStyle.getTextColor(0.7),
|
||||
fontSize: 12,
|
||||
fontWeight: '300',
|
||||
includeFontPadding: false,
|
||||
lineHeight: 16,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxInspectorStackFrame;
|
142
Libraries/YellowBox/UI/YellowBoxList.js
Normal file
142
Libraries/YellowBox/UI/YellowBoxList.js
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Dimensions = require('Dimensions');
|
||||
const React = require('React');
|
||||
const FlatList = require('FlatList');
|
||||
const SafeAreaView = require('SafeAreaView');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const View = require('View');
|
||||
const YellowBoxButton = require('YellowBoxButton');
|
||||
const YellowBoxInspector = require('YellowBoxInspector');
|
||||
const YellowBoxListRow = require('YellowBoxListRow');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type {Category} from 'YellowBoxCategory';
|
||||
import type {Registry} from 'YellowBoxRegistry';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
onDismiss: (category: Category) => void,
|
||||
onDismissAll: () => void,
|
||||
registry: Registry,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
selectedCategory: ?Category,
|
||||
|};
|
||||
|
||||
const VIEWPORT_RATIO = 0.5;
|
||||
const MAX_ITEMS = Math.floor(
|
||||
(Dimensions.get('window').height * VIEWPORT_RATIO) /
|
||||
(YellowBoxListRow.GUTTER + YellowBoxListRow.HEIGHT),
|
||||
);
|
||||
|
||||
class YellowBoxList extends React.Component<Props, State> {
|
||||
state = {
|
||||
selectedCategory: null,
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const selectedWarnings =
|
||||
this.state.selectedCategory == null
|
||||
? null
|
||||
: this.props.registry.get(this.state.selectedCategory);
|
||||
|
||||
if (selectedWarnings != null) {
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<YellowBoxInspector
|
||||
onDismiss={this._handleInspectorDismiss}
|
||||
onMinimize={this._handleInspectorMinimize}
|
||||
warnings={selectedWarnings}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const items = [];
|
||||
for (const [category, warnings] of this.props.registry) {
|
||||
items.unshift({category, warnings});
|
||||
}
|
||||
|
||||
const listStyle = {
|
||||
height:
|
||||
// Additional `0.5` so the (N + 1)th row can peek into view.
|
||||
Math.min(items.length, MAX_ITEMS + 0.5) *
|
||||
(YellowBoxListRow.GUTTER + YellowBoxListRow.HEIGHT),
|
||||
};
|
||||
|
||||
return items.length === 0 ? null : (
|
||||
<View style={styles.list}>
|
||||
<View pointerEvents="box-none" style={styles.dismissAll}>
|
||||
<YellowBoxButton
|
||||
hitSlop={{bottom: 4, left: 4, right: 4, top: 4}}
|
||||
label="Dismiss All"
|
||||
onPress={this.props.onDismissAll}
|
||||
/>
|
||||
</View>
|
||||
<FlatList
|
||||
data={items}
|
||||
keyExtractor={item => item.category}
|
||||
renderItem={({item}) => (
|
||||
<YellowBoxListRow {...item} onPress={this._handleRowPress} />
|
||||
)}
|
||||
scrollEnabled={items.length > MAX_ITEMS}
|
||||
scrollsToTop={false}
|
||||
style={listStyle}
|
||||
/>
|
||||
<SafeAreaView style={styles.safeArea} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_handleInspectorDismiss = () => {
|
||||
const category = this.state.selectedCategory;
|
||||
if (category == null) {
|
||||
return;
|
||||
}
|
||||
this.setState({selectedCategory: null}, () => {
|
||||
this.props.onDismiss(category);
|
||||
});
|
||||
};
|
||||
|
||||
_handleInspectorMinimize = () => {
|
||||
this.setState({selectedCategory: null});
|
||||
};
|
||||
|
||||
_handleRowPress = (category: Category) => {
|
||||
this.setState({selectedCategory: category});
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
list: {
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
dismissAll: {
|
||||
bottom: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 4,
|
||||
paddingEnd: 4,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
safeArea: {
|
||||
backgroundColor: YellowBoxStyle.getBackgroundColor(0.95),
|
||||
marginTop: StyleSheet.hairlineWidth,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxList;
|
100
Libraries/YellowBox/UI/YellowBoxListRow.js
Normal file
100
Libraries/YellowBox/UI/YellowBoxListRow.js
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const Text = require('Text');
|
||||
const YellowBoxPressable = require('YellowBoxPressable');
|
||||
const View = require('View');
|
||||
const YellowBoxCategory = require('YellowBoxCategory');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
const YellowBoxWarning = require('YellowBoxWarning');
|
||||
|
||||
import type {Category} from 'YellowBoxCategory';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
category: Category,
|
||||
warnings: $ReadOnlyArray<YellowBoxWarning>,
|
||||
onPress: (category: Category) => void,
|
||||
|}>;
|
||||
|
||||
class YellowBoxListRow extends React.Component<Props> {
|
||||
static GUTTER = StyleSheet.hairlineWidth;
|
||||
static HEIGHT = 48;
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
const prevProps = this.props;
|
||||
return (
|
||||
prevProps.category !== nextProps.category ||
|
||||
prevProps.onPress !== nextProps.onPress ||
|
||||
prevProps.warnings.length !== nextProps.warnings.length ||
|
||||
prevProps.warnings.some(
|
||||
(prevWarning, index) => prevWarning !== nextProps[index],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {warnings} = this.props;
|
||||
|
||||
return (
|
||||
<YellowBoxPressable onPress={this._handlePress} style={styles.root}>
|
||||
<View style={styles.content}>
|
||||
{warnings.length < 2 ? null : (
|
||||
<Text style={styles.metaText}>{'(' + warnings.length + ') '}</Text>
|
||||
)}
|
||||
<Text numberOfLines={2} style={styles.bodyText}>
|
||||
{YellowBoxCategory.render(
|
||||
warnings[warnings.length - 1].message,
|
||||
styles.substitutionText,
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</YellowBoxPressable>
|
||||
);
|
||||
}
|
||||
|
||||
_handlePress = () => {
|
||||
this.props.onPress(this.props.category);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
height: YellowBoxListRow.HEIGHT,
|
||||
justifyContent: 'center',
|
||||
marginTop: YellowBoxListRow.GUTTER,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
content: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
bodyText: {
|
||||
color: YellowBoxStyle.getTextColor(1),
|
||||
flex: 1,
|
||||
fontSize: 14,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 18,
|
||||
},
|
||||
metaText: {
|
||||
color: YellowBoxStyle.getTextColor(0.5),
|
||||
fontSize: 14,
|
||||
includeFontPadding: false,
|
||||
lineHeight: 18,
|
||||
},
|
||||
substitutionText: {
|
||||
color: YellowBoxStyle.getTextColor(0.6),
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = YellowBoxListRow;
|
86
Libraries/YellowBox/UI/YellowBoxPressable.js
Normal file
86
Libraries/YellowBox/UI/YellowBoxPressable.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const StyleSheet = require('StyleSheet');
|
||||
const TouchableWithoutFeedback = require('TouchableWithoutFeedback');
|
||||
const View = require('View');
|
||||
const YellowBoxStyle = require('YellowBoxStyle');
|
||||
|
||||
import type {PressEvent} from 'CoreEventTypes';
|
||||
import type {EdgeInsetsProp} from 'EdgeInsetsPropType';
|
||||
import type {ViewStyleProp} from 'StyleSheet';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
backgroundColor: $ReadOnly<{|
|
||||
default: string,
|
||||
pressed: string,
|
||||
|}>,
|
||||
children?: React.Node,
|
||||
hitSlop?: ?EdgeInsetsProp,
|
||||
onPress?: ?(event: PressEvent) => void,
|
||||
style?: ViewStyleProp,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
pressed: boolean,
|
||||
|};
|
||||
|
||||
class YellowBoxPressable extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
backgroundColor: {
|
||||
default: YellowBoxStyle.getBackgroundColor(0.95),
|
||||
pressed: YellowBoxStyle.getHighlightColor(1),
|
||||
},
|
||||
};
|
||||
|
||||
state = {
|
||||
pressed: false,
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const content = (
|
||||
<View
|
||||
style={StyleSheet.compose(
|
||||
{
|
||||
backgroundColor: this.state.pressed
|
||||
? this.props.backgroundColor.pressed
|
||||
: this.props.backgroundColor.default,
|
||||
},
|
||||
this.props.style,
|
||||
)}>
|
||||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
return this.props.onPress == null ? (
|
||||
content
|
||||
) : (
|
||||
<TouchableWithoutFeedback
|
||||
hitSlop={this.props.hitSlop}
|
||||
onPress={this.props.onPress}
|
||||
onPressIn={this._handlePressIn}
|
||||
onPressOut={this._handlePressOut}>
|
||||
{content}
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
_handlePressIn = () => {
|
||||
this.setState({pressed: true});
|
||||
};
|
||||
|
||||
_handlePressOut = () => {
|
||||
this.setState({pressed: false});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = YellowBoxPressable;
|
31
Libraries/YellowBox/UI/YellowBoxStyle.js
Normal file
31
Libraries/YellowBox/UI/YellowBoxStyle.js
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBoxStyle = {
|
||||
getBackgroundColor(opacity: number): string {
|
||||
return `rgba(250, 186, 48, ${opacity})`;
|
||||
},
|
||||
|
||||
getDividerColor(opacity: number): string {
|
||||
return `rgba(255, 255, 255, ${opacity})`;
|
||||
},
|
||||
|
||||
getHighlightColor(opacity: number): string {
|
||||
return `rgba(252, 176, 29, ${opacity})`;
|
||||
},
|
||||
|
||||
getTextColor(opacity: number): string {
|
||||
return `rgba(255, 255, 255, ${opacity})`;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = YellowBoxStyle;
|
132
Libraries/YellowBox/YellowBox.js
Normal file
132
Libraries/YellowBox/YellowBox.js
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Platform = require('Platform');
|
||||
const RCTLog = require('RCTLog');
|
||||
const React = require('React');
|
||||
const YellowBoxList = require('YellowBoxList');
|
||||
const YellowBoxRegistry = require('YellowBoxRegistry');
|
||||
|
||||
import type {Category} from 'YellowBoxCategory';
|
||||
import type {Registry, Subscription} from 'YellowBoxRegistry';
|
||||
|
||||
type Props = $ReadOnly<{||}>;
|
||||
type State = {|
|
||||
registry: ?Registry,
|
||||
|};
|
||||
|
||||
const {error, warn} = console;
|
||||
|
||||
/**
|
||||
* YellowBox displays warnings at the bottom of the screen.
|
||||
*
|
||||
* Warnings help guard against subtle yet significant issues that can impact the
|
||||
* quality of the app. This "in your face" style of warning allows developers to
|
||||
* notice and correct these issues as quickly as possible.
|
||||
*
|
||||
* YellowBox is only enabled in `__DEV__`. Set the following flag to disable it:
|
||||
*
|
||||
* console.disableYellowBox = true;
|
||||
*
|
||||
* Ignore specific warnings by calling:
|
||||
*
|
||||
* YellowBox.ignoreWarnings(['Warning: ...']);
|
||||
*
|
||||
* Strings supplied to `YellowBox.ignoreWarnings` only need to be a substring of
|
||||
* the ignored warning messages.
|
||||
*/
|
||||
class YellowBox extends React.Component<Props, State> {
|
||||
static ignoreWarnings(patterns: $ReadOnlyArray<string>): void {
|
||||
YellowBoxRegistry.addIgnorePatterns(patterns);
|
||||
}
|
||||
|
||||
static install(): void {
|
||||
(console: any).error = function(...args) {
|
||||
error.call(console, ...args);
|
||||
// Show YellowBox for the `warning` module.
|
||||
if (typeof args[0] === 'string' && args[0].startsWith('Warning: ')) {
|
||||
registerWarning(...args);
|
||||
}
|
||||
};
|
||||
|
||||
(console: any).warn = function(...args) {
|
||||
warn.call(console, ...args);
|
||||
registerWarning(...args);
|
||||
};
|
||||
|
||||
if ((console: any).disableYellowBox === true) {
|
||||
YellowBoxRegistry.setDisabled(true);
|
||||
}
|
||||
(Object.defineProperty: any)(console, 'disableYellowBox', {
|
||||
configurable: true,
|
||||
get: () => YellowBoxRegistry.isDisabled(),
|
||||
set: value => YellowBoxRegistry.setDisabled(value),
|
||||
});
|
||||
|
||||
if (Platform.isTesting) {
|
||||
(console: any).disableYellowBox = true;
|
||||
}
|
||||
|
||||
RCTLog.setWarningHandler((...args) => {
|
||||
registerWarning(...args);
|
||||
});
|
||||
}
|
||||
|
||||
static uninstall(): void {
|
||||
(console: any).error = error;
|
||||
(console: any).warn = error;
|
||||
delete (console: any).disableYellowBox;
|
||||
}
|
||||
|
||||
_subscription: ?Subscription;
|
||||
|
||||
state = {
|
||||
registry: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
// TODO: Ignore warnings that fire when rendering `YellowBox` itself.
|
||||
return this.state.registry == null ? null : (
|
||||
<YellowBoxList
|
||||
onDismiss={this._handleDismiss}
|
||||
onDismissAll={this._handleDismissAll}
|
||||
registry={this.state.registry}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this._subscription = YellowBoxRegistry.observe(registry => {
|
||||
this.setState({registry});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this._subscription != null) {
|
||||
this._subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
_handleDismiss = (category: Category): void => {
|
||||
YellowBoxRegistry.delete(category);
|
||||
};
|
||||
|
||||
_handleDismissAll(): void {
|
||||
YellowBoxRegistry.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function registerWarning(...args): void {
|
||||
YellowBoxRegistry.add({args, framesToPop: 2});
|
||||
}
|
||||
|
||||
module.exports = YellowBox;
|
77
Libraries/YellowBox/__tests__/YellowBox-test.js
Normal file
77
Libraries/YellowBox/__tests__/YellowBox-test.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails oncall+react_native
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const YellowBox = require('YellowBox');
|
||||
const YellowBoxRegistry = require('YellowBoxRegistry');
|
||||
|
||||
describe('YellowBox', () => {
|
||||
const {error, warn} = console;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
(console: any).error = jest.fn();
|
||||
(console: any).warn = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
YellowBox.uninstall();
|
||||
(console: any).error = error;
|
||||
(console: any).warn = warn;
|
||||
});
|
||||
|
||||
it('can set `disableYellowBox` after installing', () => {
|
||||
expect((console: any).disableYellowBox).toBe(undefined);
|
||||
|
||||
YellowBox.install();
|
||||
|
||||
expect((console: any).disableYellowBox).toBe(false);
|
||||
expect(YellowBoxRegistry.isDisabled()).toBe(false);
|
||||
|
||||
(console: any).disableYellowBox = true;
|
||||
|
||||
expect((console: any).disableYellowBox).toBe(true);
|
||||
expect(YellowBoxRegistry.isDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('can set `disableYellowBox` before installing', () => {
|
||||
expect((console: any).disableYellowBox).toBe(undefined);
|
||||
|
||||
(console: any).disableYellowBox = true;
|
||||
YellowBox.install();
|
||||
|
||||
expect((console: any).disableYellowBox).toBe(true);
|
||||
expect(YellowBoxRegistry.isDisabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('registers warnings', () => {
|
||||
jest.mock('YellowBoxRegistry');
|
||||
|
||||
YellowBox.install();
|
||||
|
||||
expect(YellowBoxRegistry.add).not.toBeCalled();
|
||||
(console: any).warn('...');
|
||||
expect(YellowBoxRegistry.add).toBeCalled();
|
||||
});
|
||||
|
||||
it('registers errors beginning with "Warning: "', () => {
|
||||
jest.mock('YellowBoxRegistry');
|
||||
|
||||
YellowBox.install();
|
||||
|
||||
(console: any).error('...');
|
||||
expect(YellowBoxRegistry.add).not.toBeCalled();
|
||||
|
||||
(console: any).error('Warning: ...');
|
||||
expect(YellowBoxRegistry.add).toBeCalled();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user