diff --git a/Libraries/ReactNative/AppContainer.js b/Libraries/ReactNative/AppContainer.js index ad5f8c06a..58ffb095f 100644 --- a/Libraries/ReactNative/AppContainer.js +++ b/Libraries/ReactNative/AppContainer.js @@ -127,4 +127,11 @@ const styles = StyleSheet.create({ }, }); +if (__DEV__) { + if (!global.__RCTProfileIsProfiling) { + const YellowBox = require('YellowBox'); + YellowBox.install(); + } +} + module.exports = AppContainer; diff --git a/Libraries/ReactNative/YellowBox.js b/Libraries/ReactNative/YellowBox.js deleted file mode 100644 index 01a4bd82d..000000000 --- a/Libraries/ReactNative/YellowBox.js +++ /dev/null @@ -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, - symbolicated: boolean, -}; - -const _warningEmitter = new EventEmitter(); -const _warningMap: Map = new Map(); -const IGNORED_WARNINGS: Array = []; - -/** - * 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 ): - * - * 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 ? ( - {'(' + count + ') '} - ) : null; - - return ( - - - - {countText} - {warning} - - - - ); -}; - -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 = ''; - } - - return ( - - - {fileName}:{lineNumber} - - - ); -}; - -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 = ( - - {stacktrace.map((frame, ii) => )} - - ); - } - - return ( - - - - {countSentence} - - - {stacktraceVisible ? '\u{25BC}' : '\u{25B6}'} Stacktrace - - - - - {stacktraceList} - {warning} - - - - Minimize - - - Dismiss - - - - - ); -}; - -class YellowBox extends React.Component< - mixed, - { - stacktraceVisible: boolean, - inspecting: ?string, - warningMap: Map, - }, -> { - _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): 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 ? ( - 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( - 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 ( - - {!inspector && - rows.length > 0 && ( - this.dismissWarning(null)}> - Dismiss All - - )} - - {rows} - - {inspector} - - ); - } -} - -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; diff --git a/Libraries/StyleSheet/StyleSheetTypes.js b/Libraries/StyleSheet/StyleSheetTypes.js index 321bdc869..cf385bb7d 100644 --- a/Libraries/StyleSheet/StyleSheetTypes.js +++ b/Libraries/StyleSheet/StyleSheetTypes.js @@ -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|} diff --git a/Libraries/UTFSequence.js b/Libraries/UTFSequence.js index f1e9e28e5..5c450cd2b 100644 --- a/Libraries/UTFSequence.js +++ b/Libraries/UTFSequence.js @@ -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; diff --git a/Libraries/YellowBox/Data/YellowBoxCategory.js b/Libraries/YellowBox/Data/YellowBoxCategory.js new file mode 100644 index 000000000..5e22fa96b --- /dev/null +++ b/Libraries/YellowBox/Data/YellowBoxCategory.js @@ -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, + ): $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({prevPart}); + } + + const substititionPart = content.substr( + substitution.offset, + substitution.length, + ); + elements.push( + + {substititionPart} + , + ); + + return substitution.offset + substitution.length; + }, + 0, + ); + + if (lastOffset < content.length - 1) { + const lastPart = content.substr(lastOffset); + elements.push({lastPart}); + } + + return elements; + }, +}; + +module.exports = YellowBoxCategory; diff --git a/Libraries/YellowBox/Data/YellowBoxRegistry.js b/Libraries/YellowBox/Data/YellowBoxRegistry.js new file mode 100644 index 000000000..9c8e11079 --- /dev/null +++ b/Libraries/YellowBox/Data/YellowBoxRegistry.js @@ -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>; + +export type Observer = (registry: Registry) => void; + +export type Subscription = $ReadOnly<{| + unsubscribe: () => void, +|}>; + +const observers: Set<{observer: Observer}> = new Set(); +const ignorePatterns: Set = 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, + 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): 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; diff --git a/Libraries/YellowBox/Data/YellowBoxSymbolication.js b/Libraries/YellowBox/Data/YellowBoxSymbolication.js new file mode 100644 index 000000000..c97b52857 --- /dev/null +++ b/Libraries/YellowBox/Data/YellowBoxSymbolication.js @@ -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; + +const cache: Map> = new Map(); + +const YellowBoxSymbolication = { + symbolicate(stack: Stack): Promise { + 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; diff --git a/Libraries/YellowBox/Data/YellowBoxWarning.js b/Libraries/YellowBox/Data/YellowBoxWarning.js new file mode 100644 index 000000000..6841bc9da --- /dev/null +++ b/Libraries/YellowBox/Data/YellowBoxWarning.js @@ -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, + 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; diff --git a/Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js b/Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js new file mode 100644 index 000000000..5c7ab61b4 --- /dev/null +++ b/Libraries/YellowBox/Data/__tests__/YellowBoxCategory-test.js @@ -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, + }, + ], + }, + }); + }); +}); diff --git a/Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js b/Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js new file mode 100644 index 000000000..af596ce4e --- /dev/null +++ b/Libraries/YellowBox/Data/__tests__/YellowBoxRegistry-test.js @@ -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); + }); +}); diff --git a/Libraries/YellowBox/Data/__tests__/YellowBoxSymbolication-test.js b/Libraries/YellowBox/Data/__tests__/YellowBoxSymbolication-test.js new file mode 100644 index 000000000..c4ca0ad53 --- /dev/null +++ b/Libraries/YellowBox/Data/__tests__/YellowBoxSymbolication-test.js @@ -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>, + Promise>, +> = (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); + }); +}); diff --git a/Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js b/Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js new file mode 100644 index 000000000..8918741b1 --- /dev/null +++ b/Libraries/YellowBox/Data/__tests__/YellowBoxWarning-test.js @@ -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>, + Promise>, + >, +|} = (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); + }); +}); diff --git a/Libraries/YellowBox/UI/YellowBoxButton.js b/Libraries/YellowBox/UI/YellowBoxButton.js new file mode 100644 index 000000000..6ea9f4525 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxButton.js @@ -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 => ( + + + {props.label} + + +); + +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; diff --git a/Libraries/YellowBox/UI/YellowBoxImageSource.js b/Libraries/YellowBox/UI/YellowBoxImageSource.js new file mode 100644 index 000000000..dca95fa82 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxImageSource.js @@ -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; diff --git a/Libraries/YellowBox/UI/YellowBoxInspector.js b/Libraries/YellowBox/UI/YellowBoxInspector.js new file mode 100644 index 000000000..aa0c742d7 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxInspector.js @@ -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, +|}>; + +type State = {| + selectedIndex: number, +|}; + +class YellowBoxInspector extends React.Component { + _symbolication: ?SymbolicationRequest; + + state = { + selectedIndex: 0, + }; + + render(): React.Node { + const {warnings} = this.props; + const {selectedIndex} = this.state; + + const warning = warnings[selectedIndex]; + + return ( + + + + + + Warning + + + {YellowBoxCategory.render( + warning.message, + styles.substitutionText, + )} + + + + + Stack + + + {warning.getAvailableStack().map((frame, index) => ( + { + openFileInEditor(frame.file, frame.lineNumber); + } + : null + } + /> + ))} + + + + + ); + } + + 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; diff --git a/Libraries/YellowBox/UI/YellowBoxInspectorFooter.js b/Libraries/YellowBox/UI/YellowBoxInspectorFooter.js new file mode 100644 index 000000000..71a45702a --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxInspectorFooter.js @@ -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 => ( + + + + Minimize + + + + + + Dismiss + + + + +); + +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; diff --git a/Libraries/YellowBox/UI/YellowBoxInspectorHeader.js b/Libraries/YellowBox/UI/YellowBoxInspectorHeader.js new file mode 100644 index 000000000..4ed124eab --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxInspectorHeader.js @@ -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, +|}>; + +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 ( + + props.onSelectIndex(prevIndex)} + /> + + {titleText} + + props.onSelectIndex(nextIndex)} + /> + + ); +}; + +const YellowBoxInspectorHeaderButton = ( + props: $ReadOnly<{| + disabled: boolean, + label: React.Node, + onPress?: ?() => void, + |}>, +): React.Node => ( + + {props.disabled ? null : ( + {props.label} + )} + +); + +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; diff --git a/Libraries/YellowBox/UI/YellowBoxInspectorSourceMapStatus.js b/Libraries/YellowBox/UI/YellowBoxInspectorSourceMapStatus.js new file mode 100644 index 000000000..831305d74 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxInspectorSourceMapStatus.js @@ -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 { + 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 : ( + + + Source Map + + ); + } + + 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; diff --git a/Libraries/YellowBox/UI/YellowBoxInspectorStackFrame.js b/Libraries/YellowBox/UI/YellowBoxInspectorStackFrame.js new file mode 100644 index 000000000..8ee0d04b9 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxInspectorStackFrame.js @@ -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 ( + + {frame.methodName} + + {`${getFrameLocation(frame.file)}:${frame.lineNumber}${ + frame.column == null ? '' : ':' + frame.column + }`} + + + ); +}; + +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; diff --git a/Libraries/YellowBox/UI/YellowBoxList.js b/Libraries/YellowBox/UI/YellowBoxList.js new file mode 100644 index 000000000..63b321cde --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxList.js @@ -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 { + state = { + selectedCategory: null, + }; + + render(): React.Node { + const selectedWarnings = + this.state.selectedCategory == null + ? null + : this.props.registry.get(this.state.selectedCategory); + + if (selectedWarnings != null) { + return ( + + + + ); + } + + 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 : ( + + + + + item.category} + renderItem={({item}) => ( + + )} + scrollEnabled={items.length > MAX_ITEMS} + scrollsToTop={false} + style={listStyle} + /> + + + ); + } + + _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; diff --git a/Libraries/YellowBox/UI/YellowBoxListRow.js b/Libraries/YellowBox/UI/YellowBoxListRow.js new file mode 100644 index 000000000..e59d0e89e --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxListRow.js @@ -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, + onPress: (category: Category) => void, +|}>; + +class YellowBoxListRow extends React.Component { + 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 ( + + + {warnings.length < 2 ? null : ( + {'(' + warnings.length + ') '} + )} + + {YellowBoxCategory.render( + warnings[warnings.length - 1].message, + styles.substitutionText, + )} + + + + ); + } + + _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; diff --git a/Libraries/YellowBox/UI/YellowBoxPressable.js b/Libraries/YellowBox/UI/YellowBoxPressable.js new file mode 100644 index 000000000..39149c251 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxPressable.js @@ -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 { + static defaultProps = { + backgroundColor: { + default: YellowBoxStyle.getBackgroundColor(0.95), + pressed: YellowBoxStyle.getHighlightColor(1), + }, + }; + + state = { + pressed: false, + }; + + render(): React.Node { + const content = ( + + {this.props.children} + + ); + return this.props.onPress == null ? ( + content + ) : ( + + {content} + + ); + } + + _handlePressIn = () => { + this.setState({pressed: true}); + }; + + _handlePressOut = () => { + this.setState({pressed: false}); + }; +} + +module.exports = YellowBoxPressable; diff --git a/Libraries/YellowBox/UI/YellowBoxStyle.js b/Libraries/YellowBox/UI/YellowBoxStyle.js new file mode 100644 index 000000000..2ceec6f28 --- /dev/null +++ b/Libraries/YellowBox/UI/YellowBoxStyle.js @@ -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; diff --git a/Libraries/YellowBox/YellowBox.js b/Libraries/YellowBox/YellowBox.js new file mode 100644 index 000000000..45a6dbc3b --- /dev/null +++ b/Libraries/YellowBox/YellowBox.js @@ -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 { + static ignoreWarnings(patterns: $ReadOnlyArray): 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 : ( + + ); + } + + 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; diff --git a/Libraries/YellowBox/__tests__/YellowBox-test.js b/Libraries/YellowBox/__tests__/YellowBox-test.js new file mode 100644 index 000000000..e22168b24 --- /dev/null +++ b/Libraries/YellowBox/__tests__/YellowBox-test.js @@ -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(); + }); +});