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:
Tim Yung 2018-06-11 18:20:52 -07:00 committed by Facebook Github Bot
parent f8b4850425
commit d0219a0301
25 changed files with 2306 additions and 558 deletions

View File

@ -127,4 +127,11 @@ const styles = StyleSheet.create({
},
});
if (__DEV__) {
if (!global.__RCTProfileIsProfiling) {
const YellowBox = require('YellowBox');
YellowBox.install();
}
}
module.exports = AppContainer;

View File

@ -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;

View File

@ -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|}

View File

@ -20,6 +20,7 @@ const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationI
* - Less chance of typos.
*/
const UTFSequence = deepFreezeAndThrowOnMutationInDev({
BOM: '\ufeff', // byte order mark
BULLET: '\u2022', // bullet: &#8226;
BULLET_SP: '\u00A0\u2022\u00A0', // &nbsp;&#8226;&nbsp;
MIDDOT: '\u00B7', // normal middle dot: &middot;
@ -31,6 +32,8 @@ const UTFSequence = deepFreezeAndThrowOnMutationInDev({
NDASH_SP: '\u00A0\u2013\u00A0', // &nbsp;&ndash;&nbsp;
NBSP: '\u00A0', // non-breaking space: &nbsp;
PIZZA: '\uD83C\uDF55',
TRIANGLE_LEFT: '\u25c0', // black left-pointing triangle
TRIANGLE_RIGHT: '\u25b6', // black right-pointing triangle
});
module.exports = UTFSequence;

View 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;

View 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;

View 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;

View 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;

View 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,
},
],
},
});
});
});

View 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);
});
});

View 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.
*
* @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);
});
});

View 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);
});
});

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();
});
});