first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,517 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {ExtendedError} from '../../Core/ExtendedError';
import type {LogLevel} from './LogBoxLog';
import type {
Category,
ComponentStack,
ComponentStackType,
ExtendedExceptionData,
Message,
} from './parseLogBoxLog';
import DebuggerSessionObserver from '../../../src/private/devsupport/rndevtools/FuseboxSessionObserver';
import parseErrorStack from '../../Core/Devtools/parseErrorStack';
import NativeLogBox from '../../NativeModules/specs/NativeLogBox';
import LogBoxLog from './LogBoxLog';
import {parseLogBoxException} from './parseLogBoxLog';
import * as React from 'react';
export type LogBoxLogs = Set<LogBoxLog>;
export type LogData = $ReadOnly<{
level: LogLevel,
message: Message,
category: Category,
componentStack: ComponentStack,
componentStackType: ComponentStackType | null,
stack?: string,
}>;
export type Observer = (
$ReadOnly<{
logs: LogBoxLogs,
isDisabled: boolean,
selectedLogIndex: number,
}>,
) => void;
export type IgnorePattern = string | RegExp;
export type Subscription = $ReadOnly<{
unsubscribe: () => void,
}>;
export type WarningInfo = {
finalFormat: string,
forceDialogImmediately: boolean,
suppressDialog_LEGACY: boolean,
suppressCompletely: boolean,
monitorEvent: string | null,
monitorListVersion: number,
monitorSampleRate: number,
};
export type WarningFilter = (format: string) => WarningInfo;
type AppInfo = $ReadOnly<{
appVersion: string,
engine: string,
onPress?: ?() => void,
}>;
const observers: Set<{observer: Observer, ...}> = new Set();
const ignorePatterns: Set<IgnorePattern> = new Set();
let appInfo: ?() => AppInfo = null;
let logs: LogBoxLogs = new Set();
let updateTimeout: $FlowFixMe | null = null;
let _isDisabled = false;
let _selectedIndex = -1;
let hasShownFuseboxWarningsMigrationMessage = false;
let hostTargetSessionObserverSubscription = null;
let warningFilter: WarningFilter = function (format) {
return {
finalFormat: format,
forceDialogImmediately: false,
suppressDialog_LEGACY: false,
suppressCompletely: false,
monitorEvent: 'warning_unhandled',
monitorListVersion: 0,
monitorSampleRate: 1,
};
};
const LOGBOX_ERROR_MESSAGE =
'An error was thrown when attempting to render log messages via LogBox.';
function getNextState() {
return {
logs,
isDisabled: _isDisabled,
selectedLogIndex: _selectedIndex,
};
}
export function reportLogBoxError(
error: ExtendedError,
componentStack?: string,
): void {
const ExceptionsManager = require('../../Core/ExceptionsManager').default;
error.message = `${LOGBOX_ERROR_MESSAGE}\n\n${error.message}`;
if (componentStack != null) {
error.componentStack = componentStack;
}
ExceptionsManager.handleException(error, /* isFatal */ true);
}
export function isLogBoxErrorMessage(message: string): boolean {
return typeof message === 'string' && message.includes(LOGBOX_ERROR_MESSAGE);
}
export function isMessageIgnored(message: string): boolean {
for (const pattern of ignorePatterns) {
if (
(pattern instanceof RegExp && pattern.test(message)) ||
(typeof pattern === 'string' && message.includes(pattern))
) {
return true;
}
}
return false;
}
function handleUpdate(): void {
if (updateTimeout == null) {
updateTimeout = setImmediate(() => {
updateTimeout = null;
const nextState = getNextState();
observers.forEach(({observer}) => observer(nextState));
});
}
}
function appendNewLog(newLog: LogBoxLog) {
// Don't want store these logs because they trigger a
// state update when we add them to the store.
if (isMessageIgnored(newLog.message.content)) {
return;
}
// If the next log has the same category as the previous one
// then roll it up into the last log in the list by incrementing
// the count (similar to how Chrome does it).
const lastLog = Array.from(logs).pop();
if (lastLog && lastLog.category === newLog.category) {
lastLog.incrementCount();
handleUpdate();
return;
}
if (newLog.level === 'fatal') {
// If possible, to avoid jank, we don't want to open the error before
// it's symbolicated. To do that, we optimistically wait for
// symbolication for up to a second before adding the log.
const OPTIMISTIC_WAIT_TIME = 1000;
let addPendingLog: ?() => void = () => {
logs.add(newLog);
if (_selectedIndex < 0) {
setSelectedLog(logs.size - 1);
} else {
handleUpdate();
}
addPendingLog = null;
};
const optimisticTimeout = setTimeout(() => {
if (addPendingLog) {
addPendingLog();
}
}, OPTIMISTIC_WAIT_TIME);
newLog.symbolicate(status => {
if (addPendingLog && status !== 'PENDING') {
addPendingLog();
clearTimeout(optimisticTimeout);
} else if (status !== 'PENDING') {
// The log has already been added but we need to trigger a render.
handleUpdate();
}
});
} else if (newLog.level === 'syntax') {
logs.add(newLog);
setSelectedLog(logs.size - 1);
} else {
logs.add(newLog);
handleUpdate();
}
}
export function addLog(log: LogData): void {
if (hostTargetSessionObserverSubscription == null) {
hostTargetSessionObserverSubscription = DebuggerSessionObserver.subscribe(
hasActiveSession => {
if (hasActiveSession) {
clearWarnings();
} else {
// Reset the flag so that we can show the message again if new warning was emitted
hasShownFuseboxWarningsMigrationMessage = false;
}
},
);
}
// If Host has Fusebox support
if (log.level === 'warn' && global.__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__) {
// And there is no active debugging session
if (!DebuggerSessionObserver.hasActiveSession()) {
showFuseboxWarningsMigrationMessageOnce();
}
// Don't show LogBox warnings when Host has active debugging session
return;
}
const errorForStackTrace = new Error();
// Parsing logs are expensive so we schedule this
// otherwise spammy logs would pause rendering.
setImmediate(() => {
try {
const stack = parseErrorStack(log.stack ?? errorForStackTrace?.stack);
appendNewLog(
new LogBoxLog({
level: log.level,
message: log.message,
isComponentError: false,
stack,
category: log.category,
componentStack: log.componentStack,
componentStackType: log.componentStackType || 'legacy',
}),
);
} catch (error) {
reportLogBoxError(error);
}
});
}
export function addException(error: ExtendedExceptionData): void {
// Parsing logs are expensive so we schedule this
// otherwise spammy logs would pause rendering.
setImmediate(() => {
try {
appendNewLog(new LogBoxLog(parseLogBoxException(error)));
} catch (loggingError) {
reportLogBoxError(loggingError);
}
});
}
export function symbolicateLogNow(log: LogBoxLog) {
log.symbolicate(() => {
handleUpdate();
});
}
export function retrySymbolicateLogNow(log: LogBoxLog) {
log.retrySymbolicate(() => {
handleUpdate();
});
}
export function symbolicateLogLazy(log: LogBoxLog) {
log.symbolicate();
}
export function clear(): void {
if (logs.size > 0) {
logs = new Set();
setSelectedLog(-1);
}
}
export function setSelectedLog(proposedNewIndex: number): void {
const oldIndex = _selectedIndex;
let newIndex = proposedNewIndex;
const logArray = Array.from(logs);
let index = logArray.length - 1;
while (index >= 0) {
// The latest syntax error is selected and displayed before all other logs.
if (logArray[index].level === 'syntax') {
newIndex = index;
break;
}
index -= 1;
}
_selectedIndex = newIndex;
handleUpdate();
if (NativeLogBox) {
setTimeout(() => {
if (oldIndex < 0 && newIndex >= 0) {
NativeLogBox.show();
} else if (oldIndex >= 0 && newIndex < 0) {
NativeLogBox.hide();
}
}, 0);
}
}
export function clearWarnings(): void {
const newLogs = Array.from(logs).filter(log => log.level !== 'warn');
if (newLogs.length !== logs.size) {
logs = new Set(newLogs);
setSelectedLog(-1);
handleUpdate();
}
}
export function clearErrors(): void {
const newLogs = Array.from(logs).filter(
log => log.level !== 'error' && log.level !== 'fatal',
);
if (newLogs.length !== logs.size) {
logs = new Set(newLogs);
setSelectedLog(-1);
}
}
export function dismiss(log: LogBoxLog): void {
if (logs.has(log)) {
logs.delete(log);
handleUpdate();
}
}
export function setWarningFilter(filter: WarningFilter): void {
warningFilter = filter;
}
export function setAppInfo(info: () => AppInfo): void {
appInfo = info;
}
export function getAppInfo(): ?AppInfo {
return appInfo != null ? appInfo() : null;
}
export function checkWarningFilter(format: string): WarningInfo {
return warningFilter(format);
}
export function getIgnorePatterns(): $ReadOnlyArray<IgnorePattern> {
return Array.from(ignorePatterns);
}
export function addIgnorePatterns(
patterns: $ReadOnlyArray<IgnorePattern>,
): void {
const existingSize = ignorePatterns.size;
// The same pattern may be added multiple times, but adding a new pattern
// can be expensive so let's find only the ones that are new.
patterns.forEach((pattern: IgnorePattern) => {
if (pattern instanceof RegExp) {
for (const existingPattern of ignorePatterns) {
if (
existingPattern instanceof RegExp &&
existingPattern.toString() === pattern.toString()
) {
return;
}
}
ignorePatterns.add(pattern);
}
ignorePatterns.add(pattern);
});
if (ignorePatterns.size === existingSize) {
return;
}
// We need to recheck all of the existing logs.
// This allows adding an ignore pattern anywhere in the codebase.
// Without this, if you ignore a pattern after the a log is created,
// then we would keep showing the log.
logs = new Set(
Array.from(logs).filter(log => !isMessageIgnored(log.message.content)),
);
handleUpdate();
}
export function setDisabled(value: boolean): void {
if (value === _isDisabled) {
return;
}
_isDisabled = value;
handleUpdate();
}
export function isDisabled(): boolean {
return _isDisabled;
}
export function observe(observer: Observer): Subscription {
const subscription = {observer};
observers.add(subscription);
observer(getNextState());
return {
unsubscribe(): void {
observers.delete(subscription);
},
};
}
type LogBoxStateSubscriptionProps = $ReadOnly<{}>;
type LogBoxStateSubscriptionState = $ReadOnly<{
logs: LogBoxLogs,
isDisabled: boolean,
hasError: boolean,
selectedLogIndex: number,
}>;
type SubscribedComponent = React.ComponentType<
$ReadOnly<{
logs: $ReadOnlyArray<LogBoxLog>,
isDisabled: boolean,
selectedLogIndex: number,
}>,
>;
export function withSubscription(
WrappedComponent: SubscribedComponent,
): React.ComponentType<{}> {
class LogBoxStateSubscription extends React.Component<
LogBoxStateSubscriptionProps,
LogBoxStateSubscriptionState,
> {
static getDerivedStateFromError(): {hasError: boolean} {
return {hasError: true};
}
componentDidCatch(err: Error, errorInfo: {componentStack: string, ...}) {
/* $FlowFixMe[class-object-subtyping] added when improving typing for
* this parameters */
// $FlowFixMe[incompatible-type]
reportLogBoxError(err, errorInfo.componentStack);
}
_subscription: ?Subscription;
state: LogBoxStateSubscriptionState = {
logs: new Set(),
isDisabled: false,
hasError: false,
selectedLogIndex: -1,
};
render(): React.Node {
if (this.state.hasError) {
// This happens when the component failed to render, in which case we delegate to the native redbox.
// We can't show anyback fallback UI here, because the error may be with <View> or <Text>.
return null;
}
return (
<WrappedComponent
logs={Array.from(this.state.logs)}
isDisabled={this.state.isDisabled}
selectedLogIndex={this.state.selectedLogIndex}
/>
);
}
componentDidMount(): void {
this._subscription = observe(data => {
this.setState(data);
});
}
componentWillUnmount(): void {
if (this._subscription != null) {
this._subscription.unsubscribe();
}
}
}
return LogBoxStateSubscription;
}
function showFuseboxWarningsMigrationMessageOnce() {
if (hasShownFuseboxWarningsMigrationMessage) {
return;
}
hasShownFuseboxWarningsMigrationMessage = true;
const NativeDevSettings =
require('../../NativeModules/specs/NativeDevSettings').default;
appendNewLog(
new LogBoxLog({
level: 'warn',
message: {
content: 'Open debugger to view warnings.',
substitutions: [],
},
isComponentError: false,
stack: [],
category: 'fusebox-warnings-migration',
componentStack: [],
onNotificationPress: () => {
if (NativeDevSettings.openDebugger) {
NativeDevSettings.openDebugger();
}
},
}),
);
}

View File

@@ -0,0 +1,279 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {Stack} from './LogBoxSymbolication';
import type {
Category,
CodeFrame,
ComponentStack,
ComponentStackType,
Message,
} from './parseLogBoxLog';
import * as LogBoxSymbolication from './LogBoxSymbolication';
type SymbolicationStatus = 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED';
export type LogLevel = 'warn' | 'error' | 'fatal' | 'syntax';
// TODO: once component stacks are fully supported, we can refactor
// ComponentStack to just be Stack and remove these conversions fns.
function convertComponentStateToStack(componentStack: ComponentStack): Stack {
return componentStack.map(frame => ({
column: frame?.location?.column,
file: frame.fileName,
lineNumber: frame?.location?.row,
methodName: frame.content,
collapse: false,
}));
}
function convertStackToComponentStack(stack: Stack): ComponentStack {
const componentStack = [];
for (let i = 0; i < stack.length; i++) {
const frame = stack[i];
// NOTE: Skip stack frames missing location.
if (frame.lineNumber != null && frame.column != null) {
componentStack.push({
fileName: frame?.file || '',
location: {
row: frame.lineNumber,
column: frame.column,
},
content: frame.methodName,
collapse: false,
});
}
}
return componentStack;
}
export type LogBoxLogData = $ReadOnly<{
level: LogLevel,
type?: ?string,
message: Message,
stack: Stack,
category: string,
componentStackType?: ComponentStackType,
componentStack: ComponentStack,
codeFrame?: ?CodeFrame,
isComponentError: boolean,
extraData?: mixed,
onNotificationPress?: ?() => void,
}>;
class LogBoxLog {
message: Message;
type: ?string;
category: Category;
componentStack: ComponentStack;
componentStackType: ComponentStackType;
stack: Stack;
count: number;
level: LogLevel;
codeFrame: ?CodeFrame;
componentCodeFrame: ?CodeFrame;
isComponentError: boolean;
extraData: mixed | void;
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',
};
symbolicatedComponentStack:
| $ReadOnly<{error: null, componentStack: null, status: 'NONE'}>
| $ReadOnly<{error: null, componentStack: null, status: 'PENDING'}>
| $ReadOnly<{
error: null,
componentStack: ComponentStack,
status: 'COMPLETE',
}>
| $ReadOnly<{error: Error, componentStack: null, status: 'FAILED'}> = {
error: null,
componentStack: null,
status: 'NONE',
};
onNotificationPress: ?() => void;
constructor(data: LogBoxLogData) {
this.level = data.level;
this.type = data.type;
this.message = data.message;
this.stack = data.stack;
this.category = data.category;
this.componentStack = data.componentStack;
this.componentStackType = data.componentStackType || 'legacy';
this.codeFrame = data.codeFrame;
this.isComponentError = data.isComponentError;
this.extraData = data.extraData;
this.count = 1;
this.onNotificationPress = data.onNotificationPress;
}
incrementCount(): void {
this.count += 1;
}
getAvailableStack(): Stack {
return this.symbolicated.status === 'COMPLETE'
? this.symbolicated.stack
: this.stack;
}
getAvailableComponentStack(): ComponentStack {
if (this.componentStackType === 'legacy') {
return this.componentStack;
}
return this.symbolicatedComponentStack.status === 'COMPLETE'
? this.symbolicatedComponentStack.componentStack
: this.componentStack;
}
retrySymbolicate(callback?: (status: SymbolicationStatus) => void): void {
let retry = false;
if (this.symbolicated.status !== 'COMPLETE') {
LogBoxSymbolication.deleteStack(this.stack);
retry = true;
}
if (this.symbolicatedComponentStack.status !== 'COMPLETE') {
LogBoxSymbolication.deleteStack(
convertComponentStateToStack(this.componentStack),
);
retry = true;
}
if (retry) {
this.handleSymbolicate(callback);
}
}
symbolicate(callback?: (status: SymbolicationStatus) => void): void {
if (this.symbolicated.status === 'NONE') {
this.handleSymbolicate(callback);
}
}
handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void {
if (
this.symbolicated.status !== 'PENDING' &&
this.symbolicated.status !== 'COMPLETE'
) {
this.updateStatus(null, null, null, callback);
LogBoxSymbolication.symbolicate(this.stack, this.extraData).then(
data => {
this.updateStatus(null, data?.stack, data?.codeFrame, callback);
},
error => {
this.updateStatus(error, null, null, callback);
},
);
}
if (
this.componentStack != null &&
this.componentStackType === 'stack' &&
this.symbolicatedComponentStack.status !== 'PENDING' &&
this.symbolicatedComponentStack.status !== 'COMPLETE'
) {
this.updateComponentStackStatus(null, null, null, callback);
const componentStackFrames = convertComponentStateToStack(
this.componentStack,
);
LogBoxSymbolication.symbolicate(componentStackFrames, []).then(
data => {
this.updateComponentStackStatus(
null,
convertStackToComponentStack(data.stack),
data?.codeFrame,
callback,
);
},
error => {
this.updateComponentStackStatus(error, null, null, callback);
},
);
}
}
updateStatus(
error: ?Error,
stack: ?Stack,
codeFrame: ?CodeFrame,
callback?: (status: SymbolicationStatus) => void,
): void {
const lastStatus = this.symbolicated.status;
if (error != null) {
this.symbolicated = {
error,
stack: null,
status: 'FAILED',
};
} else if (stack != null) {
if (codeFrame) {
this.codeFrame = codeFrame;
}
this.symbolicated = {
error: null,
stack,
status: 'COMPLETE',
};
} else {
this.symbolicated = {
error: null,
stack: null,
status: 'PENDING',
};
}
if (callback && lastStatus !== this.symbolicated.status) {
callback(this.symbolicated.status);
}
}
updateComponentStackStatus(
error: ?Error,
componentStack: ?ComponentStack,
codeFrame: ?CodeFrame,
callback?: (status: SymbolicationStatus) => void,
): void {
const lastStatus = this.symbolicatedComponentStack.status;
if (error != null) {
this.symbolicatedComponentStack = {
error,
componentStack: null,
status: 'FAILED',
};
} else if (componentStack != null) {
if (codeFrame) {
this.componentCodeFrame = codeFrame;
}
this.symbolicatedComponentStack = {
error: null,
componentStack,
status: 'COMPLETE',
};
} else {
this.symbolicatedComponentStack = {
error: null,
componentStack: null,
status: 'PENDING',
};
}
if (callback && lastStatus !== this.symbolicatedComponentStack.status) {
callback(this.symbolicatedComponentStack.status);
}
}
}
export default LogBoxLog;

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {SymbolicatedStackTrace} from '../../Core/Devtools/symbolicateStackTrace';
import type {StackFrame} from '../../Core/NativeExceptionsManager';
import symbolicateStackTrace from '../../Core/Devtools/symbolicateStackTrace';
export type Stack = Array<StackFrame>;
const cache: Map<Stack, Promise<SymbolicatedStackTrace>> = new Map();
/**
* Sanitize because sometimes, `symbolicateStackTrace` gives us invalid values.
*/
const sanitize = ({
stack: maybeStack,
codeFrame,
}: SymbolicatedStackTrace): SymbolicatedStackTrace => {
if (!Array.isArray(maybeStack)) {
throw new Error('Expected stack to be an array.');
}
const stack: Array<StackFrame> = [];
for (const maybeFrame of maybeStack) {
let collapse = false;
if ('collapse' in maybeFrame) {
if (typeof maybeFrame.collapse !== 'boolean') {
throw new Error('Expected stack frame `collapse` to be a boolean.');
}
collapse = maybeFrame.collapse;
}
stack.push({
column: maybeFrame.column,
file: maybeFrame.file,
lineNumber: maybeFrame.lineNumber,
methodName: maybeFrame.methodName,
collapse,
});
}
return {stack, codeFrame};
};
export function deleteStack(stack: Stack): void {
cache.delete(stack);
}
export function symbolicate(
stack: Stack,
extraData?: mixed,
): Promise<SymbolicatedStackTrace> {
let promise = cache.get(stack);
if (promise == null) {
promise = symbolicateStackTrace(stack, extraData).then(sanitize);
cache.set(stack, promise);
}
return promise;
}

View File

@@ -0,0 +1,516 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {ExceptionData} from '../../Core/NativeExceptionsManager';
import type {LogBoxLogData} from './LogBoxLog';
import parseErrorStack from '../../Core/Devtools/parseErrorStack';
import UTFSequence from '../../UTFSequence';
import stringifySafe from '../../Utilities/stringifySafe';
import ansiRegex from 'ansi-regex';
const ANSI_REGEX = ansiRegex().source;
const RE_TRANSFORM_ERROR = /^TransformError /;
const RE_COMPONENT_STACK_LINE = /\n {4}(in|at) /;
const RE_COMPONENT_STACK_LINE_GLOBAL = /\n {4}(in|at) /g;
const RE_COMPONENT_STACK_LINE_OLD = / {4}in/;
const RE_COMPONENT_STACK_LINE_NEW = / {4}at/;
const RE_COMPONENT_STACK_LINE_STACK_FRAME = /@.*\n/;
// "TransformError " (Optional) and either "SyntaxError: " or "ReferenceError: "
// Capturing groups:
// 1: error message
// 2: file path
// 3: line number
// 4: column number
// \n\n
// 5: code frame
const RE_BABEL_TRANSFORM_ERROR_FORMAT =
/^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/;
// Capturing groups:
// 1: component name
// "at"
// 2: file path including extension
// 3: line number
const RE_COMPONENT_STACK_WITH_SOURCE =
/(.*) \(at (.*\.(?:js|jsx|ts|tsx)):([\d]+)\)/;
// Capturing groups:
// 1: component name
// "at"
// 2: parent component name
const RE_COMPONENT_STACK_NO_SOURCE = /(.*) \(created by .*\)/;
// Capturing groups:
// - non-capturing "TransformError " (optional)
// - non-capturing Error message
// 1: file path
// 2: file name
// 3: error message
// 4: code frame, which includes code snippet indicators or terminal escape sequences for formatting.
const RE_BABEL_CODE_FRAME_ERROR_FORMAT =
// eslint-disable-next-line no-control-regex
/^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\u{001b}[\s\S]+)/u;
// Capturing groups:
// - non-capturing "InternalError Metro has encountered an error:"
// 1: error title
// 2: error message
// 3: file path
// 4: line number
// 5: column number
// 6: code frame, which includes code snippet indicators or terminal escape sequences for formatting.
const RE_METRO_ERROR_FORMAT =
/^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+)/u;
// https://github.com/babel/babel/blob/33dbb85e9e9fe36915273080ecc42aee62ed0ade/packages/babel-code-frame/src/index.ts#L183-L184
const RE_BABEL_CODE_FRAME_MARKER_PATTERN = new RegExp(
[
// Beginning of a line (per 'm' flag)
'^',
// Optional ANSI escapes for colors
`(?:${ANSI_REGEX})*`,
// Marker
'>',
// Optional ANSI escapes for colors
`(?:${ANSI_REGEX})*`,
// Left padding for line number
' +',
// Line number
'[0-9]+',
// Gutter
' \\|',
].join(''),
'm',
);
export function hasComponentStack(args: $ReadOnlyArray<mixed>): boolean {
for (const arg of args) {
if (typeof arg === 'string' && isComponentStack(arg)) {
return true;
}
}
return false;
}
export type ExtendedExceptionData = ExceptionData & {
isComponentError: boolean,
...
};
export type Category = string;
export type CodeFrame = $ReadOnly<{
content: string,
location: ?{
row: number,
column: number,
...
},
fileName: string,
// TODO: When React switched to using call stack frames,
// we gained the ability to use the collapse flag, but
// it is not integrated into the LogBox UI.
collapse?: boolean,
}>;
export type Message = $ReadOnly<{
content: string,
substitutions: $ReadOnlyArray<
$ReadOnly<{
length: number,
offset: number,
}>,
>,
}>;
export type ComponentStack = $ReadOnlyArray<CodeFrame>;
export type ComponentStackType = 'legacy' | 'stack';
const SUBSTITUTION = UTFSequence.BOM + '%s';
export function parseInterpolation(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) {
// Don't stringify a string type.
// It adds quotation mark wrappers around the string,
// which causes the LogBox to look odd.
const substitution =
typeof substitutions[substitutionIndex] === 'string'
? substitutions[substitutionIndex]
: 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(arg => {
// Don't stringify a string type.
// It adds quotation mark wrappers around the string,
// which causes the LogBox to look odd.
return typeof arg === 'string' ? arg : stringifySafe(arg);
});
categoryParts.push(...remainingArgs);
contentParts.push(...remainingArgs);
return {
category: categoryParts.join(' '),
message: {
content: contentParts.join(' '),
substitutions: substitutionOffsets,
},
};
}
function isComponentStack(consoleArgument: string) {
const isOldComponentStackFormat =
RE_COMPONENT_STACK_LINE_OLD.test(consoleArgument);
const isNewComponentStackFormat =
RE_COMPONENT_STACK_LINE_NEW.test(consoleArgument);
const isNewJSCComponentStackFormat =
RE_COMPONENT_STACK_LINE_STACK_FRAME.test(consoleArgument);
return (
isOldComponentStackFormat ||
isNewComponentStackFormat ||
isNewJSCComponentStackFormat
);
}
export function parseComponentStack(message: string): {
type: ComponentStackType,
stack: ComponentStack,
} {
// In newer versions of React, the component stack is formatted as a call stack frame.
// First try to parse the component stack as a call stack frame, and if that doesn't
// work then we'll fallback to the old custom component stack format parsing.
const stack = parseErrorStack(message);
if (stack && stack.length > 0) {
return {
type: 'stack',
stack: stack.map(frame => ({
content: frame.methodName,
collapse: frame.collapse || false,
fileName: frame.file == null ? 'unknown' : frame.file,
location: {
column: frame.column == null ? -1 : frame.column,
row: frame.lineNumber == null ? -1 : frame.lineNumber,
},
})),
};
}
const legacyStack = message
.split(RE_COMPONENT_STACK_LINE_GLOBAL)
.map(s => {
if (!s) {
return null;
}
const match = s.match(RE_COMPONENT_STACK_WITH_SOURCE);
if (match) {
let [content, fileName, row] = match.slice(1);
return {
content,
fileName,
location: {column: -1, row: parseInt(row, 10)},
};
}
// In some cases, the component stack doesn't have a source.
const matchWithoutSource = s.match(RE_COMPONENT_STACK_NO_SOURCE);
if (matchWithoutSource) {
return {
content: matchWithoutSource[1],
fileName: '',
location: null,
};
}
return null;
})
.filter(Boolean);
return {
type: 'legacy',
stack: legacyStack,
};
}
export function parseLogBoxException(
error: ExtendedExceptionData,
): LogBoxLogData {
const message =
error.originalMessage != null ? error.originalMessage : 'Unknown';
const metroInternalError = message.match(RE_METRO_ERROR_FORMAT);
if (metroInternalError) {
const [content, fileName, row, column, codeFrame] =
metroInternalError.slice(1);
return {
level: 'fatal',
type: 'Metro Error',
stack: [],
isComponentError: false,
componentStackType: 'legacy',
componentStack: [],
codeFrame: {
fileName,
location: {
row: parseInt(row, 10),
column: parseInt(column, 10),
},
content: codeFrame,
},
message: {
content,
substitutions: [],
},
category: `${fileName}-${row}-${column}`,
extraData: error.extraData,
};
}
const babelTransformError = message.match(RE_BABEL_TRANSFORM_ERROR_FORMAT);
if (babelTransformError) {
// Transform errors are thrown from inside the Babel transformer.
const [fileName, content, row, column, codeFrame] =
babelTransformError.slice(1);
return {
level: 'syntax',
stack: [],
isComponentError: false,
componentStackType: 'legacy',
componentStack: [],
codeFrame: {
fileName,
location: {
row: parseInt(row, 10),
column: parseInt(column, 10),
},
content: codeFrame,
},
message: {
content,
substitutions: [],
},
category: `${fileName}-${row}-${column}`,
extraData: error.extraData,
};
}
// Perform a cheap match first before trying to parse the full message, which
// can get expensive for arbitrary input.
if (RE_BABEL_CODE_FRAME_MARKER_PATTERN.test(message)) {
const babelCodeFrameError = message.match(RE_BABEL_CODE_FRAME_ERROR_FORMAT);
if (babelCodeFrameError) {
// Codeframe errors are thrown from any use of buildCodeFrameError.
const [fileName, content, codeFrame] = babelCodeFrameError.slice(1);
return {
level: 'syntax',
stack: [],
isComponentError: false,
componentStackType: 'legacy',
componentStack: [],
codeFrame: {
fileName,
location: null, // We are not given the location.
content: codeFrame,
},
message: {
content,
substitutions: [],
},
category: `${fileName}-${1}-${1}`,
extraData: error.extraData,
};
}
}
if (message.match(RE_TRANSFORM_ERROR)) {
return {
level: 'syntax',
stack: error.stack,
isComponentError: error.isComponentError,
componentStackType: 'legacy',
componentStack: [],
message: {
content: message,
substitutions: [],
},
category: message,
extraData: error.extraData,
};
}
const componentStack = error.componentStack;
if (error.isFatal || error.isComponentError) {
if (componentStack != null) {
const {type, stack} = parseComponentStack(componentStack);
return {
level: 'fatal',
stack: error.stack,
isComponentError: error.isComponentError,
componentStackType: type,
componentStack: stack,
extraData: error.extraData,
...parseInterpolation([message]),
};
} else {
return {
level: 'fatal',
stack: error.stack,
isComponentError: error.isComponentError,
componentStackType: 'legacy',
componentStack: [],
extraData: error.extraData,
...parseInterpolation([message]),
};
}
}
if (componentStack != null) {
// It is possible that console errors have a componentStack.
const {type, stack} = parseComponentStack(componentStack);
return {
level: 'error',
stack: error.stack,
isComponentError: error.isComponentError,
componentStackType: type,
componentStack: stack,
extraData: error.extraData,
...parseInterpolation([message]),
};
}
// Most `console.error` calls won't have a componentStack. We parse them like
// regular logs which have the component stack buried in the message.
return {
level: 'error',
stack: error.stack,
isComponentError: error.isComponentError,
extraData: error.extraData,
...parseLogBoxLog([message]),
};
}
export function withoutANSIColorStyles(message: mixed): mixed {
if (typeof message !== 'string') {
return message;
}
return message.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
'',
);
}
export function parseLogBoxLog(args: $ReadOnlyArray<mixed>): {
componentStack: ComponentStack,
componentStackType: ComponentStackType,
category: Category,
message: Message,
} {
const message = withoutANSIColorStyles(args[0]);
let argsWithoutComponentStack: Array<mixed> = [];
let componentStack: ComponentStack = [];
let componentStackType = 'legacy';
// Extract component stack from warnings like "Some warning%s".
if (
typeof message === 'string' &&
message.slice(-2) === '%s' &&
args.length > 0
) {
const lastArg = args[args.length - 1];
if (typeof lastArg === 'string' && isComponentStack(lastArg)) {
argsWithoutComponentStack = args.slice(0, -1);
argsWithoutComponentStack[0] = message.slice(0, -2);
const {type, stack} = parseComponentStack(lastArg);
componentStack = stack;
componentStackType = type;
}
}
if (componentStack.length === 0 && argsWithoutComponentStack.length === 0) {
// Try finding the component stack elsewhere.
for (const arg of args) {
if (typeof arg === 'string' && isComponentStack(arg)) {
// Strip out any messages before the component stack.
let messageEndIndex = arg.search(RE_COMPONENT_STACK_LINE);
if (messageEndIndex < 0) {
// Handle JSC component stacks.
messageEndIndex = arg.search(/\n/);
}
if (messageEndIndex > 0) {
argsWithoutComponentStack.push(arg.slice(0, messageEndIndex));
}
const {type, stack} = parseComponentStack(arg);
componentStack = stack;
componentStackType = type;
} else {
argsWithoutComponentStack.push(arg);
}
}
}
return {
...parseInterpolation(argsWithoutComponentStack),
componentStack,
/* $FlowFixMe[incompatible-type] Natural Inference rollout. See
* https://fburl.com/workplace/6291gfvu */
componentStackType,
};
}

28
node_modules/react-native/Libraries/LogBox/LogBox.d.ts generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
export interface LogBoxStatic {
/**
* Silence any logs that match the given strings or regexes.
*/
ignoreLogs(patterns: (string | RegExp)[]): void;
/**
* Toggle error and warning notifications
* Note: this only disables notifications, uncaught errors will still open a full screen LogBox.
* @param ignore whether to ignore logs or not
*/
ignoreAllLogs(ignore?: boolean): void;
install(): void;
uninstall(): void;
}
export const LogBox: LogBoxStatic;
export type LogBox = LogBoxStatic;

284
node_modules/react-native/Libraries/LogBox/LogBox.js generated vendored Normal file
View File

@@ -0,0 +1,284 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
import type {IgnorePattern, LogData} from './Data/LogBoxData';
import type {ExtendedExceptionData} from './Data/parseLogBoxLog';
import Platform from '../Utilities/Platform';
import RCTLog from '../Utilities/RCTLog';
import * as React from 'react';
export type {LogData, ExtendedExceptionData, IgnorePattern};
let LogBox;
interface ILogBox {
install(): void;
uninstall(): void;
isInstalled(): boolean;
ignoreLogs($ReadOnlyArray<IgnorePattern>): void;
ignoreAllLogs(value?: boolean): void;
clearAllLogs(): void;
addLog(log: LogData): void;
addConsoleLog(level: 'warn' | 'error', ...args: Array<mixed>): void;
addException(error: ExtendedExceptionData): void;
}
/**
* LogBox displays logs in the app.
*/
if (__DEV__) {
const LogBoxData = require('./Data/LogBoxData');
const {
parseLogBoxLog,
parseComponentStack,
} = require('./Data/parseLogBoxLog');
let originalConsoleWarn;
let consoleWarnImpl: (...args: Array<mixed>) => void;
let isLogBoxInstalled: boolean = false;
LogBox = {
install(): void {
if (isLogBoxInstalled) {
return;
}
isLogBoxInstalled = true;
if (global.RN$registerExceptionListener != null) {
global.RN$registerExceptionListener(
(error: ExtendedExceptionData & {preventDefault: () => mixed}) => {
if (global.RN$isRuntimeReady?.() || !error.isFatal) {
error.preventDefault();
addException(error);
}
},
);
}
// Trigger lazy initialization of module.
require('../NativeModules/specs/NativeLogBox');
// IMPORTANT: we only overwrite `console.error` and `console.warn` once.
// When we uninstall we keep the same reference and only change its
// internal implementation
const isFirstInstall = originalConsoleWarn == null;
if (isFirstInstall) {
// We only patch warning for legacy reasons.
// This will be removed in the future, once warnings
// are fully moved to fusebox. Error handling is done
// via the ExceptionManager.
originalConsoleWarn = console.warn.bind(console);
// $FlowExpectedError[cannot-write]
console.warn = (...args) => {
consoleWarnImpl(...args);
};
}
consoleWarnImpl = registerWarning;
if (Platform.isTesting) {
LogBoxData.setDisabled(true);
}
RCTLog.setWarningHandler((...args) => {
registerWarning(...args);
});
},
uninstall(): void {
if (!isLogBoxInstalled) {
return;
}
isLogBoxInstalled = false;
// IMPORTANT: we don't re-assign to `console` in case the method has been
// decorated again after installing LogBox. E.g.:
// Before uninstalling: original > LogBox > OtherErrorHandler
// After uninstalling: original > LogBox (noop) > OtherErrorHandler
consoleWarnImpl = originalConsoleWarn;
},
isInstalled(): boolean {
return isLogBoxInstalled;
},
/**
* Silence any logs that match the given strings or regexes.
*/
ignoreLogs(patterns: $ReadOnlyArray<IgnorePattern>): void {
LogBoxData.addIgnorePatterns(patterns);
},
/**
* Toggle error and warning notifications
* Note: this only disables notifications, uncaught errors will still open a full screen LogBox.
* @param ignore whether to ignore logs or not
*/
ignoreAllLogs(value?: ?boolean): void {
LogBoxData.setDisabled(value == null ? true : value);
},
clearAllLogs(): void {
LogBoxData.clear();
},
addLog(log: LogData): void {
if (isLogBoxInstalled) {
LogBoxData.addLog(log);
}
},
addConsoleLog(level: 'warn' | 'error', ...args: Array<mixed>) {
if (isLogBoxInstalled) {
let filteredLevel: 'warn' | 'error' | 'fatal' = level;
try {
let format = args[0];
if (typeof format === 'string') {
const filterResult =
require('../LogBox/Data/LogBoxData').checkWarningFilter(
// For legacy reasons, we strip the warning prefix from the message.
// Can remove this once we remove the warning module altogether.
format.replace(/^Warning: /, ''),
);
if (filterResult.monitorEvent !== 'warning_unhandled') {
if (filterResult.suppressCompletely) {
return;
}
if (filterResult.suppressDialog_LEGACY === true) {
filteredLevel = 'warn';
} else if (filterResult.forceDialogImmediately === true) {
filteredLevel = 'fatal'; // Do not downgrade. These are real bugs with same severity as throws.
}
args[0] = filterResult.finalFormat;
}
}
const result = parseLogBoxLog(args);
const category = result.category;
const message = result.message;
let componentStackType = result.componentStackType;
let componentStack = result.componentStack;
if (
(!componentStack || componentStack.length === 0) &&
// $FlowExpectedError[prop-missing]
React.captureOwnerStack
) {
const ownerStack = React.captureOwnerStack();
if (ownerStack != null && ownerStack.length > 0) {
const parsedComponentStack = parseComponentStack(ownerStack);
componentStack = parsedComponentStack.stack;
componentStackType = parsedComponentStack.type;
}
}
if (!LogBoxData.isMessageIgnored(message.content)) {
LogBoxData.addLog({
level: filteredLevel,
category,
message,
componentStack,
componentStackType,
});
}
} catch (err) {
LogBoxData.reportLogBoxError(err);
}
}
},
addException,
};
function addException(error: ExtendedExceptionData): void {
if (isLogBoxInstalled) {
LogBoxData.addException(error);
}
}
const isRCTLogAdviceWarning = (...args: Array<mixed>) => {
// RCTLogAdvice is a native logging function designed to show users
// a message in the console, but not show it to them in Logbox.
return typeof args[0] === 'string' && args[0].startsWith('(ADVICE)');
};
const registerWarning = (...args: Array<mixed>): void => {
// Let warnings within LogBox itself fall through.
if (LogBoxData.isLogBoxErrorMessage(String(args[0]))) {
return;
} else {
// Be sure to pass LogBox warnings through.
originalConsoleWarn(...args);
}
try {
if (!isRCTLogAdviceWarning(...args)) {
const {category, message, componentStack, componentStackType} =
parseLogBoxLog(args);
if (!LogBoxData.isMessageIgnored(message.content)) {
LogBoxData.addLog({
level: 'warn',
category,
message,
componentStack,
componentStackType,
});
}
}
} catch (err) {
LogBoxData.reportLogBoxError(err);
}
};
} else {
LogBox = {
install(): void {
// Do nothing.
},
uninstall(): void {
// Do nothing.
},
isInstalled(): boolean {
return false;
},
ignoreLogs(patterns: $ReadOnlyArray<IgnorePattern>): void {
// Do nothing.
},
ignoreAllLogs(value?: ?boolean): void {
// Do nothing.
},
clearAllLogs(): void {
// Do nothing.
},
addLog(log: LogData): void {
// Do nothing.
},
addConsoleLog(level: 'warn' | 'error', ...args: Array<mixed>): void {
// Do nothing.
},
addException(error: ExtendedExceptionData): void {
// Do nothing.
},
};
}
export default (LogBox: ILogBox);

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type LogBoxLog from './Data/LogBoxLog';
import View from '../Components/View/View';
import StyleSheet from '../StyleSheet/StyleSheet';
import * as LogBoxData from './Data/LogBoxData';
import LogBoxInspector from './UI/LogBoxInspector';
import * as React from 'react';
type Props = $ReadOnly<{
logs: $ReadOnlyArray<LogBoxLog>,
selectedLogIndex: number,
isDisabled?: ?boolean,
}>;
export class _LogBoxInspectorContainer extends React.Component<Props> {
render(): React.Node {
return (
<View style={StyleSheet.absoluteFill}>
<LogBoxInspector
onDismiss={this._handleDismiss}
onMinimize={this._handleMinimize}
onChangeSelectedIndex={this._handleSetSelectedLog}
logs={this.props.logs}
selectedIndex={this.props.selectedLogIndex}
/>
</View>
);
}
_handleDismiss = (): void => {
// Here we handle the cases when the log is dismissed and it
// was either the last log, or when the current index
// is now outside the bounds of the log array.
const {selectedLogIndex, logs} = this.props;
const logsArray = Array.from(logs);
if (selectedLogIndex != null) {
if (logsArray.length - 1 <= 0) {
LogBoxData.setSelectedLog(-1);
} else if (selectedLogIndex >= logsArray.length - 1) {
LogBoxData.setSelectedLog(selectedLogIndex - 1);
}
LogBoxData.dismiss(logsArray[selectedLogIndex]);
}
};
_handleMinimize = (): void => {
LogBoxData.setSelectedLog(-1);
};
_handleSetSelectedLog = (index: number): void => {
LogBoxData.setSelectedLog(index);
};
}
export default (LogBoxData.withSubscription(
_LogBoxInspectorContainer,
): React.ComponentType<{}>);

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import SafeAreaView from '../../src/private/components/safeareaview/SafeAreaView_INTERNAL_DO_NOT_USE';
import View from '../Components/View/View';
import StyleSheet from '../StyleSheet/StyleSheet';
import * as LogBoxData from './Data/LogBoxData';
import LogBoxLog from './Data/LogBoxLog';
import LogBoxLogNotification from './UI/LogBoxNotification';
import * as React from 'react';
type Props = $ReadOnly<{
logs: $ReadOnlyArray<LogBoxLog>,
selectedLogIndex: number,
isDisabled?: ?boolean,
}>;
export function _LogBoxNotificationContainer(props: Props): React.Node {
const {logs} = props;
const onDismissWarns = () => {
LogBoxData.clearWarnings();
};
const onDismissErrors = () => {
LogBoxData.clearErrors();
};
const setSelectedLog = (index: number): void => {
LogBoxData.setSelectedLog(index);
};
function openLog(log: LogBoxLog) {
if (log.onNotificationPress) {
log.onNotificationPress();
return;
}
let index = logs.length - 1;
// Stop at zero because if we don't find any log, we'll open the first log.
while (index > 0 && logs[index] !== log) {
index -= 1;
}
setSelectedLog(index);
}
if (logs.length === 0 || props.isDisabled === true) {
return null;
}
const warnings = logs.filter(log => log.level === 'warn');
const errors = logs.filter(
log => log.level === 'error' || log.level === 'fatal',
);
return (
<SafeAreaView style={styles.list}>
{warnings.length > 0 && (
<View style={styles.toast}>
<LogBoxLogNotification
log={warnings[warnings.length - 1]}
level="warn"
totalLogCount={warnings.length}
onPressOpen={() => openLog(warnings[warnings.length - 1])}
onPressDismiss={onDismissWarns}
/>
</View>
)}
{errors.length > 0 && (
<View style={styles.toast}>
<LogBoxLogNotification
log={errors[errors.length - 1]}
level="error"
totalLogCount={errors.length}
onPressOpen={() => openLog(errors[errors.length - 1])}
onPressDismiss={onDismissErrors}
/>
</View>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
list: {
bottom: 20,
left: 10,
right: 10,
position: 'absolute',
},
toast: {
borderRadius: 8,
marginBottom: 5,
overflow: 'hidden',
},
});
export default (LogBoxData.withSubscription(
_LogBoxNotificationContainer,
): React.ComponentType<{}>);

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import {ansiToJson} from 'anser';
import * as React from 'react';
// Afterglow theme from https://iterm2colorschemes.com/
const COLORS = {
'ansi-black': 'rgb(27, 27, 27)',
'ansi-red': 'rgb(187, 86, 83)',
'ansi-green': 'rgb(144, 157, 98)',
'ansi-yellow': 'rgb(234, 193, 121)',
'ansi-blue': 'rgb(125, 169, 199)',
'ansi-magenta': 'rgb(176, 101, 151)',
'ansi-cyan': 'rgb(140, 220, 216)',
// Instead of white, use the default color provided to the component
// 'ansi-white': 'rgb(216, 216, 216)',
'ansi-bright-black': 'rgb(98, 98, 98)',
'ansi-bright-red': 'rgb(187, 86, 83)',
'ansi-bright-green': 'rgb(144, 157, 98)',
'ansi-bright-yellow': 'rgb(234, 193, 121)',
'ansi-bright-blue': 'rgb(125, 169, 199)',
'ansi-bright-magenta': 'rgb(176, 101, 151)',
'ansi-bright-cyan': 'rgb(140, 220, 216)',
'ansi-bright-white': 'rgb(247, 247, 247)',
};
const LRM = '\u200E'; // Left-to-Right Mark
export default function Ansi({
text,
style,
}: {
text: string,
style: TextStyleProp,
...
}): React.Node {
let commonWhitespaceLength = Infinity;
const parsedLines = text.split(/\n/).map(line =>
ansiToJson(line, {
json: true,
remove_empty: true,
use_classes: true,
}),
);
parsedLines.map(lines => {
// The third item on each line includes the whitespace of the source code.
// We are looking for the least amount of common whitespace to trim all lines.
// Example: Array [" ", " 96 |", " text", ...]
const match = lines[2] && lines[2]?.content?.match(/^ +/);
const whitespaceLength = (match && match[0]?.length) || 0;
if (whitespaceLength < commonWhitespaceLength) {
commonWhitespaceLength = whitespaceLength;
}
});
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
const getText = (content, key) => {
if (key === 0) {
return LRM + content;
} else if (key === 1) {
// Remove the vertical bar after line numbers
return content.replace(/\| $/, ' ');
} else if (key === 2 && commonWhitespaceLength < Infinity) {
// Remove common whitespace at the beginning of the line
return content.slice(commonWhitespaceLength);
} else {
return content;
}
};
return (
<View style={styles.container}>
{parsedLines.map((items, i) => (
<View style={styles.line} key={i}>
<Text style={styles.text}>
{items.map((bundle, key) => {
const textStyle =
bundle.fg && COLORS[bundle.fg]
? {
backgroundColor: bundle.bg && COLORS[bundle.bg],
color: bundle.fg && COLORS[bundle.fg],
}
: {
backgroundColor: bundle.bg && COLORS[bundle.bg],
};
return (
<Text
id="logbox_codeframe_contents_text"
style={[style, textStyle]}
key={key}>
{getText(bundle.content, key)}
</Text>
);
})}
</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
minWidth: '100%',
direction: 'ltr',
},
line: {
flexDirection: 'row',
},
text: {
flexGrow: 1,
},
});

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import TouchableWithoutFeedback from '../../Components/Touchable/TouchableWithoutFeedback';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useState} from 'react';
type Props = $ReadOnly<{
id?: string,
backgroundColor: $ReadOnly<{
default: string,
pressed: string,
}>,
children?: React.Node,
hitSlop?: ?EdgeInsetsProp,
onPress?: ?(event: GestureResponderEvent) => void,
style?: ViewStyleProp,
}>;
function LogBoxButton(props: Props): React.Node {
const [pressed, setPressed] = useState(false);
let backgroundColor = props.backgroundColor;
if (!backgroundColor) {
backgroundColor = {
default: LogBoxStyle.getBackgroundColor(0.95),
pressed: LogBoxStyle.getBackgroundColor(0.6),
};
}
const content = (
<View
id={props.id}
style={StyleSheet.compose(
{
backgroundColor: pressed
? backgroundColor.pressed
: backgroundColor.default,
},
props.style,
)}>
{props.children}
</View>
);
return props.onPress == null ? (
content
) : (
<TouchableWithoutFeedback
hitSlop={props.hitSlop}
onPress={props.onPress}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}>
{content}
</TouchableWithoutFeedback>
);
}
export default LogBoxButton;

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import Keyboard from '../../Components/Keyboard/Keyboard';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import * as LogBoxData from '../Data/LogBoxData';
import LogBoxLog, {type LogLevel} from '../Data/LogBoxLog';
import LogBoxInspectorBody from './LogBoxInspectorBody';
import LogBoxInspectorFooter from './LogBoxInspectorFooter';
import LogBoxInspectorHeader from './LogBoxInspectorHeader';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useEffect} from 'react';
type Props = $ReadOnly<{
onDismiss: () => void,
onChangeSelectedIndex: (index: number) => void,
onMinimize: () => void,
logs: $ReadOnlyArray<LogBoxLog>,
selectedIndex: number,
fatalType?: ?LogLevel,
}>;
export default function LogBoxInspector(props: Props): React.Node {
const {logs, selectedIndex} = props;
let log = logs[selectedIndex];
useEffect(() => {
if (log) {
LogBoxData.symbolicateLogNow(log);
}
}, [log]);
useEffect(() => {
// Optimistically symbolicate the last and next logs.
if (logs.length > 1) {
const selected = selectedIndex;
const lastIndex = logs.length - 1;
const prevIndex = selected - 1 < 0 ? lastIndex : selected - 1;
const nextIndex = selected + 1 > lastIndex ? 0 : selected + 1;
LogBoxData.symbolicateLogLazy(logs[prevIndex]);
LogBoxData.symbolicateLogLazy(logs[nextIndex]);
}
}, [logs, selectedIndex]);
useEffect(() => {
Keyboard.dismiss();
}, []);
function _handleRetry() {
LogBoxData.retrySymbolicateLogNow(log);
}
if (log == null) {
return null;
}
return (
<View id="logbox_inspector" style={styles.root}>
<LogBoxInspectorHeader
onSelectIndex={props.onChangeSelectedIndex}
selectedIndex={selectedIndex}
total={logs.length}
level={log.level}
/>
<LogBoxInspectorBody log={log} onRetry={_handleRetry} />
<LogBoxInspectorFooter
onDismiss={props.onDismiss}
onMinimize={props.onMinimize}
level={log.level}
/>
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: LogBoxStyle.getTextColor(),
},
});

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import ScrollView from '../../Components/ScrollView/ScrollView';
import StyleSheet from '../../StyleSheet/StyleSheet';
import LogBoxLog from '../Data/LogBoxLog';
import LogBoxInspectorCodeFrame from './LogBoxInspectorCodeFrame';
import LogBoxInspectorMessageHeader from './LogBoxInspectorMessageHeader';
import LogBoxInspectorReactFrames from './LogBoxInspectorReactFrames';
import LogBoxInspectorStackFrames from './LogBoxInspectorStackFrames';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useEffect, useState} from 'react';
const headerTitleMap = {
warn: 'Console Warning',
error: 'Console Error',
fatal: 'Uncaught Error',
syntax: 'Syntax Error',
component: 'Render Error',
};
export default function LogBoxInspectorBody(props: {
log: LogBoxLog,
onRetry: () => void,
}): React.Node {
const [collapsed, setCollapsed] = useState(true);
useEffect(() => {
setCollapsed(true);
}, [props.log]);
const headerTitle =
props.log.type ??
headerTitleMap[props.log.isComponentError ? 'component' : props.log.level];
if (collapsed) {
return (
<>
<LogBoxInspectorMessageHeader
collapsed={collapsed}
onPress={() => setCollapsed(!collapsed)}
message={props.log.message}
level={props.log.level}
title={headerTitle}
/>
<ScrollView style={styles.scrollBody}>
<LogBoxInspectorCodeFrame
codeFrame={props.log.codeFrame}
componentCodeFrame={props.log.componentCodeFrame}
/>
<LogBoxInspectorReactFrames log={props.log} />
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
</ScrollView>
</>
);
}
return (
<ScrollView style={styles.scrollBody}>
<LogBoxInspectorMessageHeader
collapsed={collapsed}
onPress={() => setCollapsed(!collapsed)}
message={props.log.message}
level={props.log.level}
title={headerTitle}
/>
<LogBoxInspectorCodeFrame
codeFrame={props.log.codeFrame}
componentCodeFrame={props.log.componentCodeFrame}
/>
<LogBoxInspectorReactFrames log={props.log} />
<LogBoxInspectorStackFrames log={props.log} onRetry={props.onRetry} />
</ScrollView>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: LogBoxStyle.getTextColor(),
},
scrollBody: {
backgroundColor: LogBoxStyle.getBackgroundColor(0.9),
flex: 1,
},
});

View File

@@ -0,0 +1,183 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {CodeFrame} from '../Data/parseLogBoxLog';
import ScrollView from '../../Components/ScrollView/ScrollView';
import View from '../../Components/View/View';
import openFileInEditor from '../../Core/Devtools/openFileInEditor';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
import * as LogBoxData from '../Data/LogBoxData';
import AnsiHighlight from './AnsiHighlight';
import LogBoxButton from './LogBoxButton';
import LogBoxInspectorSection from './LogBoxInspectorSection';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
componentCodeFrame: ?CodeFrame,
codeFrame: ?CodeFrame,
}>;
function CodeFrameDisplay({codeFrame}: {codeFrame: CodeFrame}): React.Node {
function getFileName() {
// $FlowFixMe[incompatible-use]
const matches = /[^/]*$/.exec(codeFrame.fileName);
if (matches && matches.length > 0) {
return matches[0];
}
// $FlowFixMe[incompatible-use]
return codeFrame.fileName;
}
function getLocation() {
// $FlowFixMe[incompatible-use]
const location = codeFrame.location;
if (location != null) {
return ` (${location.row}:${
location.column + 1 /* Code frame columns are zero indexed */
})`;
}
return null;
}
return (
<View style={styles.box}>
<View style={styles.frame}>
<ScrollView horizontal contentContainerStyle={styles.contentContainer}>
<AnsiHighlight style={styles.content} text={codeFrame.content} />
</ScrollView>
</View>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundDarkColor(1),
}}
style={styles.button}
onPress={() => {
openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0);
}}>
<Text style={styles.fileText}>
{getFileName()}
{getLocation()}
</Text>
</LogBoxButton>
</View>
);
}
function LogBoxInspectorCodeFrame(props: Props): React.Node {
const {codeFrame, componentCodeFrame} = props;
let sources = [];
if (codeFrame != null) {
sources.push(codeFrame);
}
if (
componentCodeFrame != null &&
componentCodeFrame?.content !== codeFrame?.content
) {
sources.push(componentCodeFrame);
}
if (sources.length === 0) {
return null;
}
return (
<LogBoxInspectorSection
heading={sources.length > 1 ? 'Sources' : 'Source'}
action={<AppInfo />}>
{sources.map((frame, index) => (
<CodeFrameDisplay key={index} codeFrame={frame} />
))}
</LogBoxInspectorSection>
);
}
function AppInfo() {
const appInfo = LogBoxData.getAppInfo();
if (appInfo == null) {
return null;
}
return (
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: appInfo.onPress
? LogBoxStyle.getBackgroundColor(1)
: 'transparent',
}}
style={appInfoStyles.buildButton}
onPress={appInfo.onPress}>
<Text style={appInfoStyles.text}>
{appInfo.appVersion} ({appInfo.engine})
</Text>
</LogBoxButton>
);
}
const appInfoStyles = StyleSheet.create({
text: {
color: LogBoxStyle.getTextColor(0.4),
fontSize: 12,
lineHeight: 12,
},
buildButton: {
flex: 0,
flexGrow: 0,
paddingVertical: 4,
paddingHorizontal: 5,
borderRadius: 5,
marginRight: -8,
},
});
const styles = StyleSheet.create({
box: {
backgroundColor: LogBoxStyle.getBackgroundColor(),
marginLeft: 10,
marginRight: 10,
marginTop: 5,
borderRadius: 3,
},
frame: {
padding: 10,
borderBottomColor: LogBoxStyle.getTextColor(0.1),
borderBottomWidth: 1,
},
button: {
paddingTop: 10,
paddingBottom: 10,
},
contentContainer: {
minWidth: '100%',
},
content: {
color: LogBoxStyle.getTextColor(1),
fontSize: 12,
includeFontPadding: false,
lineHeight: 20,
fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}),
},
fileText: {
color: LogBoxStyle.getTextColor(0.5),
textAlign: 'center',
flex: 1,
fontSize: 12,
includeFontPadding: false,
lineHeight: 16,
fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}),
},
});
export default LogBoxInspectorCodeFrame;

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {LogLevel} from '../Data/LogBoxLog';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxInspectorFooterButton from './LogBoxInspectorFooterButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
onDismiss: () => void,
onMinimize: () => void,
level?: ?LogLevel,
}>;
export default function LogBoxInspectorFooter(props: Props): React.Node {
if (props.level === 'syntax') {
return (
<View style={styles.root}>
<View style={styles.button}>
<Text id="logbox_dismissable_text" style={styles.syntaxErrorText}>
This error cannot be dismissed.
</Text>
</View>
</View>
);
}
return (
<View style={styles.root}>
<LogBoxInspectorFooterButton
id="logbox_footer_button_dismiss"
text="Dismiss"
onPress={props.onDismiss}
/>
<LogBoxInspectorFooterButton
id="logbox_footer_button_minimize"
text="Minimize"
onPress={props.onMinimize}
/>
</View>
);
}
const styles = StyleSheet.create({
root: {
backgroundColor: LogBoxStyle.getBackgroundColor(1),
shadowColor: '#000',
shadowOffset: {width: 0, height: -2},
shadowRadius: 2,
shadowOpacity: 0.5,
flexDirection: 'row',
},
button: {
flex: 1,
},
syntaxErrorText: {
textAlign: 'center',
width: '100%',
height: 48,
fontSize: 14,
lineHeight: 20,
paddingTop: 20,
paddingBottom: 50,
fontStyle: 'italic',
color: LogBoxStyle.getTextColor(0.6),
},
});

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type ButtonProps = $ReadOnly<{
id: string,
onPress: () => void,
text: string,
}>;
export default function LogBoxInspectorFooterButton(
props: ButtonProps,
): React.Node {
return (
<SafeAreaView style={styles.button}>
<LogBoxButton
id={props.id}
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundDarkColor(),
}}
onPress={props.onPress}>
<View style={styles.buttonContent}>
<Text style={styles.buttonLabel}>{props.text}</Text>
</View>
</LogBoxButton>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
button: {
flex: 1,
},
buttonContent: {
alignItems: 'center',
height: 48,
justifyContent: 'center',
},
buttonLabel: {
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 20,
},
});

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {ViewProps} from '../../Components/View/ViewPropTypes';
import type {LogLevel} from '../Data/LogBoxLog';
import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
import LogBoxInspectorHeaderButton from './LogBoxInspectorHeaderButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
onSelectIndex: (selectedIndex: number) => void,
selectedIndex: number,
total: number,
level: LogLevel,
}>;
const LogBoxInspectorHeaderSafeArea: React.ComponentType<ViewProps> =
Platform.OS === 'android' ? View : SafeAreaView;
export default function LogBoxInspectorHeader(props: Props): React.Node {
if (props.level === 'syntax') {
return (
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
<View style={styles.header}>
<View style={styles.title}>
<Text style={styles.titleText} id="logbox_header_title_text">
Failed to compile
</Text>
</View>
</View>
</LogBoxInspectorHeaderSafeArea>
);
}
const prevIndex =
props.selectedIndex - 1 < 0 ? props.total - 1 : props.selectedIndex - 1;
const nextIndex =
props.selectedIndex + 1 > props.total - 1 ? 0 : props.selectedIndex + 1;
const titleText = `Log ${props.selectedIndex + 1} of ${props.total}`;
return (
<LogBoxInspectorHeaderSafeArea style={styles[props.level]}>
<View style={styles.header}>
<LogBoxInspectorHeaderButton
id="logbox_header_button_prev"
disabled={props.total <= 1}
level={props.level}
image={require('./LogBoxImages/chevron-left.png')}
onPress={() => props.onSelectIndex(prevIndex)}
/>
<View style={styles.title}>
<Text style={styles.titleText} id="logbox_header_title_text">
{titleText}
</Text>
</View>
<LogBoxInspectorHeaderButton
id="logbox_header_button_next"
disabled={props.total <= 1}
level={props.level}
image={require('./LogBoxImages/chevron-right.png')}
onPress={() => props.onSelectIndex(nextIndex)}
/>
</View>
</LogBoxInspectorHeaderSafeArea>
);
}
const styles = StyleSheet.create({
syntax: {
backgroundColor: LogBoxStyle.getFatalColor(),
},
fatal: {
backgroundColor: LogBoxStyle.getFatalColor(),
},
warn: {
backgroundColor: LogBoxStyle.getWarningColor(),
},
error: {
backgroundColor: LogBoxStyle.getErrorColor(),
},
header: {
flexDirection: 'row',
height: Platform.select({
android: 48,
ios: 44,
}),
},
title: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
titleText: {
color: LogBoxStyle.getTextColor(),
fontSize: 16,
fontWeight: '600',
includeFontPadding: false,
lineHeight: 20,
},
});

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {ImageSource} from '../../Image/ImageSource';
import type {LogLevel} from '../Data/LogBoxLog';
import Image from '../../Image/Image';
import StyleSheet from '../../StyleSheet/StyleSheet';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
const backgroundForLevel = (level: LogLevel) =>
({
warn: {
default: 'transparent',
pressed: LogBoxStyle.getWarningDarkColor(),
},
error: {
default: 'transparent',
pressed: LogBoxStyle.getErrorDarkColor(),
},
fatal: {
default: 'transparent',
pressed: LogBoxStyle.getFatalDarkColor(),
},
syntax: {
default: 'transparent',
pressed: LogBoxStyle.getFatalDarkColor(),
},
})[level];
export default function LogBoxInspectorHeaderButton(
props: $ReadOnly<{
id: string,
disabled: boolean,
image: ImageSource,
level: LogLevel,
onPress?: ?() => void,
}>,
): React.Node {
return (
<LogBoxButton
id={props.id}
backgroundColor={backgroundForLevel(props.level)}
onPress={props.disabled ? null : props.onPress}
style={styles.button}>
{props.disabled ? null : (
<Image source={props.image} style={styles.buttonImage} />
)}
</LogBoxButton>
);
}
const styles = StyleSheet.create({
button: {
alignItems: 'center',
aspectRatio: 1,
justifyContent: 'center',
marginTop: 5,
marginRight: 6,
marginLeft: 6,
marginBottom: -8,
borderRadius: 3,
},
buttonImage: {
height: 14,
width: 8,
tintColor: LogBoxStyle.getTextColor(),
},
});

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {LogLevel} from '../Data/LogBoxLog';
import type {Message} from '../Data/parseLogBoxLog';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxMessage from './LogBoxMessage';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
collapsed: boolean,
message: Message,
level: LogLevel,
title: string,
onPress: () => void,
}>;
const SHOW_MORE_MESSAGE_LENGTH = 300;
function LogBoxInspectorMessageHeader(props: Props): React.Node {
function renderShowMore() {
if (
props.message.content.length < SHOW_MORE_MESSAGE_LENGTH ||
!props.collapsed
) {
return null;
}
return (
<Text style={messageStyles.collapse} onPress={() => props.onPress()}>
... See More
</Text>
);
}
return (
<View style={messageStyles.body}>
<View style={messageStyles.heading}>
<Text
style={[messageStyles.headingText, messageStyles[props.level]]}
id="logbox_message_title_text">
{props.title}
</Text>
</View>
<Text style={messageStyles.bodyText} id="logbox_message_contents_text">
<LogBoxMessage
maxLength={props.collapsed ? SHOW_MORE_MESSAGE_LENGTH : Infinity}
message={props.message}
style={messageStyles.messageText}
/>
{renderShowMore()}
</Text>
</View>
);
}
const messageStyles = StyleSheet.create({
body: {
backgroundColor: LogBoxStyle.getBackgroundColor(1),
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowRadius: 2,
shadowOpacity: 0.5,
flex: 0,
},
bodyText: {
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 20,
fontWeight: '500',
paddingHorizontal: 12,
paddingBottom: 10,
},
heading: {
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 12,
marginTop: 10,
marginBottom: 5,
},
headingText: {
flex: 1,
fontSize: 20,
fontWeight: '600',
includeFontPadding: false,
lineHeight: 28,
},
warn: {
color: LogBoxStyle.getWarningColor(1),
},
error: {
color: LogBoxStyle.getErrorColor(1),
},
fatal: {
color: LogBoxStyle.getFatalColor(1),
},
syntax: {
color: LogBoxStyle.getFatalColor(1),
},
messageText: {
color: LogBoxStyle.getTextColor(0.6),
},
collapse: {
color: LogBoxStyle.getTextColor(0.7),
fontSize: 14,
fontWeight: '300',
lineHeight: 12,
},
button: {
paddingVertical: 5,
paddingHorizontal: 10,
borderRadius: 3,
},
});
export default LogBoxInspectorMessageHeader;

View File

@@ -0,0 +1,191 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type LogBoxLog from '../Data/LogBoxLog';
import View from '../../Components/View/View';
import openFileInEditor from '../../Core/Devtools/openFileInEditor';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
import LogBoxButton from './LogBoxButton';
import LogBoxInspectorSection from './LogBoxInspectorSection';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useState} from 'react';
type Props = $ReadOnly<{
log: LogBoxLog,
}>;
const BEFORE_SLASH_RE = /^(.*)[\\/]/;
// Taken from React https://github.com/facebook/react/blob/206d61f72214e8ae5b935f0bf8628491cb7f0797/packages/react-devtools-shared/src/backend/describeComponentFrame.js#L27-L41
function getPrettyFileName(path: string) {
let fileName = path.replace(BEFORE_SLASH_RE, '');
// In DEV, include code for a common special case:
// prefer "folder/index.js" instead of just "index.js".
if (/^index\./.test(fileName)) {
const match = path.match(BEFORE_SLASH_RE);
if (match) {
const pathBeforeSlash = match[1];
if (pathBeforeSlash) {
const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
// Note the below string contains a zero width space after the "/" character.
// This is to prevent browsers like Chrome from formatting the file name as a link.
// (Since this is a source link, it would not work to open the source file anyway.)
fileName = folderName + '/' + fileName;
}
}
}
return fileName;
}
function LogBoxInspectorReactFrames(props: Props): React.Node {
const [collapsed, setCollapsed] = useState(true);
if (
props.log.getAvailableComponentStack() == null ||
props.log.getAvailableComponentStack().length < 1
) {
return null;
}
function getStackList() {
if (collapsed) {
return props.log.getAvailableComponentStack().slice(0, 3);
} else {
return props.log.getAvailableComponentStack();
}
}
function getCollapseMessage() {
if (props.log.getAvailableComponentStack().length <= 3) {
return;
}
const count = props.log.getAvailableComponentStack().length - 3;
if (collapsed) {
return `See ${count} more components`;
} else {
return `Collapse ${count} components`;
}
}
return (
<LogBoxInspectorSection heading="Component Stack">
{getStackList().map((frame, index) => (
<View
// Unfortunately we don't have a unique identifier for stack traces.
key={index}
style={componentStyles.frameContainer}>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
onPress={
// Older versions of DevTools do not provide full path.
// This will not work on Windows, remove check once the
// DevTools return the full file path.
frame.fileName.startsWith('/')
? () =>
openFileInEditor(frame.fileName, frame.location?.row ?? 1)
: null
}
style={componentStyles.frame}>
<View style={componentStyles.component}>
<Text
id="logbox_component_stack_frame_text"
style={componentStyles.frameName}>
<Text style={componentStyles.bracket}>{'<'}</Text>
{frame.content}
<Text style={componentStyles.bracket}>{' />'}</Text>
</Text>
</View>
<Text style={componentStyles.frameLocation}>
{getPrettyFileName(frame.fileName)}
{frame.location ? `:${frame.location.row}` : ''}
</Text>
</LogBoxButton>
</View>
))}
<View style={componentStyles.collapseContainer}>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
onPress={() => setCollapsed(!collapsed)}
style={componentStyles.collapseButton}>
<Text style={componentStyles.collapse}>{getCollapseMessage()}</Text>
</LogBoxButton>
</View>
</LogBoxInspectorSection>
);
}
const componentStyles = StyleSheet.create({
collapseContainer: {
marginLeft: 15,
flexDirection: 'row',
},
collapseButton: {
borderRadius: 5,
},
collapse: {
color: LogBoxStyle.getTextColor(0.7),
fontSize: 12,
fontWeight: '300',
lineHeight: 20,
marginTop: 0,
paddingVertical: 5,
paddingHorizontal: 10,
},
frameContainer: {
flexDirection: 'row',
paddingHorizontal: 15,
},
frame: {
flex: 1,
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 5,
},
component: {
flexDirection: 'row',
paddingRight: 10,
},
frameName: {
fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}),
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 18,
},
bracket: {
fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}),
color: LogBoxStyle.getTextColor(0.4),
fontSize: 14,
fontWeight: '500',
includeFontPadding: false,
lineHeight: 18,
},
frameLocation: {
color: LogBoxStyle.getTextColor(0.7),
fontSize: 12,
fontWeight: '300',
includeFontPadding: false,
lineHeight: 16,
paddingLeft: 10,
},
});
export default LogBoxInspectorReactFrames;

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
heading: string,
children: React.Node,
action?: ?React.Node,
}>;
function LogBoxInspectorSection(props: Props): React.Node {
return (
<View style={styles.section}>
<View style={styles.heading}>
<Text style={styles.headingText}>{props.heading}</Text>
{props.action}
</View>
<View style={styles.body}>{props.children}</View>
</View>
);
}
const styles = StyleSheet.create({
section: {
marginTop: 15,
},
heading: {
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 12,
marginBottom: 10,
},
headingText: {
color: LogBoxStyle.getTextColor(1),
flex: 1,
fontSize: 18,
fontWeight: '600',
includeFontPadding: false,
lineHeight: 20,
},
body: {
paddingBottom: 10,
},
});
export default LogBoxInspectorSection;

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import Animated from '../../Animated/Animated';
import Easing from '../../Animated/Easing';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useEffect, useState} from 'react';
type Props = $ReadOnly<{
onPress?: ?(event: GestureResponderEvent) => void,
status: 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
}>;
function LogBoxInspectorSourceMapStatus(props: Props): React.Node {
const [state, setState] = useState({
animation: null,
rotate: null,
});
useEffect(() => {
if (props.status === 'PENDING') {
if (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,
}),
);
// $FlowFixMe[incompatible-type]
setState({
animation,
rotate: animated.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
});
animation.start();
}
} else {
if (state.animation != null) {
state.animation.stop();
setState({
animation: null,
rotate: null,
});
}
}
return () => {
if (state.animation != null) {
state.animation.stop();
}
};
}, [props.status, state.animation]);
let image;
let color;
switch (props.status) {
case 'FAILED':
image = require('./LogBoxImages/alert-triangle.png');
color = LogBoxStyle.getErrorColor(1);
break;
case 'PENDING':
image = require('./LogBoxImages/loader.png');
color = LogBoxStyle.getWarningColor(1);
break;
}
if (props.status === 'COMPLETE' || image == null) {
return null;
}
return (
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
hitSlop={{bottom: 8, left: 8, right: 8, top: 8}}
onPress={props.onPress}
style={styles.root}>
<Animated.Image
source={image}
style={[
styles.image,
{tintColor: color},
state.rotate == null || props.status !== 'PENDING'
? null
: {transform: [{rotate: state.rotate}]},
]}
/>
<Text style={[styles.text, {color}]}>Source Map</Text>
</LogBoxButton>
);
}
const styles = StyleSheet.create({
root: {
alignItems: 'center',
borderRadius: 12,
flexDirection: 'row',
height: 24,
paddingHorizontal: 8,
},
image: {
height: 14,
width: 16,
marginEnd: 4,
tintColor: LogBoxStyle.getTextColor(0.4),
},
text: {
fontSize: 12,
includeFontPadding: false,
lineHeight: 16,
},
});
export default LogBoxInspectorSourceMapStatus;

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {StackFrame} from '../../Core/NativeExceptionsManager';
import type {GestureResponderEvent} from '../../Types/CoreEventTypes';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import Platform from '../../Utilities/Platform';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
type Props = $ReadOnly<{
frame: StackFrame,
onPress?: ?(event: GestureResponderEvent) => void,
}>;
function LogBoxInspectorStackFrame(props: Props): React.Node {
const {frame, onPress} = props;
const column = frame.column != null && parseInt(frame.column, 10);
const location =
getFileName(frame.file) +
(frame.lineNumber != null
? ':' +
frame.lineNumber +
(column && !isNaN(column) ? ':' + (column + 1) : '')
: '');
return (
<View style={styles.frameContainer}>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: onPress ? LogBoxStyle.getBackgroundColor(1) : 'transparent',
}}
onPress={onPress}
style={styles.frame}>
<Text
id="logbox_stack_frame_text"
style={[styles.name, frame.collapse === true && styles.dim]}>
{frame.methodName}
</Text>
<Text
ellipsizeMode="middle"
numberOfLines={1}
style={[styles.location, frame.collapse === true && styles.dim]}>
{location}
</Text>
</LogBoxButton>
</View>
);
}
function getFileName(file: ?string) {
if (file == null) {
return '<unknown>';
}
const queryIndex = file.indexOf('?');
return file.substring(
file.lastIndexOf('/') + 1,
queryIndex === -1 ? file.length : queryIndex,
);
}
const styles = StyleSheet.create({
frameContainer: {
flexDirection: 'row',
paddingHorizontal: 15,
},
frame: {
flex: 1,
paddingVertical: 4,
paddingHorizontal: 10,
borderRadius: 5,
},
lineLocation: {
flexDirection: 'row',
},
name: {
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 18,
fontWeight: '400',
fontFamily: Platform.select({android: 'monospace', ios: 'Menlo'}),
},
location: {
color: LogBoxStyle.getTextColor(0.8),
fontSize: 12,
fontWeight: '300',
includeFontPadding: false,
lineHeight: 16,
paddingLeft: 10,
},
dim: {
color: LogBoxStyle.getTextColor(0.4),
fontWeight: '300',
},
line: {
color: LogBoxStyle.getTextColor(0.8),
fontSize: 12,
fontWeight: '300',
includeFontPadding: false,
lineHeight: 16,
},
});
export default LogBoxInspectorStackFrame;

View File

@@ -0,0 +1,222 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {StackFrame} from '../../Core/NativeExceptionsManager';
import type LogBoxLog from '../Data/LogBoxLog';
import type {Stack} from '../Data/LogBoxSymbolication';
import View from '../../Components/View/View';
import openFileInEditor from '../../Core/Devtools/openFileInEditor';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxButton from './LogBoxButton';
import LogBoxInspectorSection from './LogBoxInspectorSection';
import LogBoxInspectorSourceMapStatus from './LogBoxInspectorSourceMapStatus';
import LogBoxInspectorStackFrame from './LogBoxInspectorStackFrame';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useState} from 'react';
type Props = $ReadOnly<{
log: LogBoxLog,
onRetry: () => void,
}>;
export function getCollapseMessage(
stackFrames: Stack,
collapsed: boolean,
): string {
if (stackFrames.length === 0) {
return 'No frames to show';
}
const collapsedCount = stackFrames.reduce((count, {collapse}) => {
if (collapse === true) {
return count + 1;
}
return count;
}, 0);
if (collapsedCount === 0) {
return 'Showing all frames';
}
const framePlural = `frame${collapsedCount > 1 ? 's' : ''}`;
if (collapsedCount === stackFrames.length) {
return collapsed
? `See${
collapsedCount > 1 ? ' all ' : ' '
}${collapsedCount} collapsed ${framePlural}`
: `Collapse${
collapsedCount > 1 ? ' all ' : ' '
}${collapsedCount} ${framePlural}`;
} else {
return collapsed
? `See ${collapsedCount} more ${framePlural}`
: `Collapse ${collapsedCount} ${framePlural}`;
}
}
function LogBoxInspectorStackFrames(props: Props): React.Node {
const [collapsed, setCollapsed] = useState(() => {
// Only collapse frames initially if some frames are not collapsed.
return props.log.getAvailableStack().some(({collapse}) => !collapse);
});
function getStackList() {
if (collapsed === true) {
return props.log.getAvailableStack().filter(({collapse}) => !collapse);
} else {
return props.log.getAvailableStack();
}
}
if (props.log.getAvailableStack().length === 0) {
return null;
}
return (
<LogBoxInspectorSection
heading="Call Stack"
action={
<LogBoxInspectorSourceMapStatus
onPress={
props.log.symbolicated.status === 'FAILED' ? props.onRetry : null
}
status={props.log.symbolicated.status}
/>
}>
{props.log.symbolicated.status !== 'COMPLETE' && (
<View style={stackStyles.hintBox}>
<Text style={stackStyles.hintText}>
This call stack is not symbolicated. Some features are unavailable
such as viewing the function name or tapping to open files.
</Text>
</View>
)}
<StackFrameList
list={getStackList()}
status={props.log.symbolicated.status}
/>
<StackFrameFooter
onPress={() => setCollapsed(!collapsed)}
message={getCollapseMessage(props.log.getAvailableStack(), collapsed)}
/>
</LogBoxInspectorSection>
);
}
function StackFrameList(props: {
list: Stack | Array<StackFrame>,
status: string | 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
}) {
return (
<>
{props.list.map((frame, index) => {
const {file, lineNumber} = frame;
return (
<LogBoxInspectorStackFrame
key={index}
frame={frame}
onPress={
props.status === 'COMPLETE' && file != null && lineNumber != null
? () => openFileInEditor(file, lineNumber)
: null
}
/>
);
})}
</>
);
}
function StackFrameFooter(
props: $ReadOnly<{message: string, onPress: () => void}>,
) {
return (
<View style={stackStyles.collapseContainer}>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
onPress={props.onPress}
style={stackStyles.collapseButton}>
<Text style={stackStyles.collapse}>{props.message}</Text>
</LogBoxButton>
</View>
);
}
const stackStyles = StyleSheet.create({
section: {
marginTop: 15,
},
heading: {
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 12,
marginBottom: 10,
},
headingText: {
color: LogBoxStyle.getTextColor(1),
flex: 1,
fontSize: 20,
fontWeight: '600',
includeFontPadding: false,
lineHeight: 20,
},
body: {
paddingBottom: 10,
},
bodyText: {
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 18,
fontWeight: '500',
paddingHorizontal: 27,
},
hintText: {
color: LogBoxStyle.getTextColor(0.7),
fontSize: 13,
includeFontPadding: false,
lineHeight: 18,
fontWeight: '400',
marginHorizontal: 10,
},
hintBox: {
backgroundColor: LogBoxStyle.getBackgroundColor(),
marginHorizontal: 10,
paddingHorizontal: 5,
paddingVertical: 10,
borderRadius: 5,
marginBottom: 5,
},
collapseContainer: {
marginLeft: 15,
flexDirection: 'row',
},
collapseButton: {
borderRadius: 5,
},
collapse: {
color: LogBoxStyle.getTextColor(0.7),
fontSize: 12,
fontWeight: '300',
lineHeight: 20,
marginTop: 0,
paddingHorizontal: 10,
paddingVertical: 5,
},
});
export default LogBoxInspectorStackFrames;

View File

@@ -0,0 +1,169 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {TextStyleProp} from '../../StyleSheet/StyleSheet';
import type {Message} from '../Data/parseLogBoxLog';
import Linking from '../../Linking/Linking';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import * as React from 'react';
type Props = {
message: Message,
style: TextStyleProp,
plaintext?: ?boolean,
maxLength?: ?number,
...
};
type Range = {
lowerBound: number,
upperBound: number,
};
function getLinkRanges(string: string): $ReadOnlyArray<Range> {
const regex = /https?:\/\/[^\s$.?#].[^\s]*/gi;
const matches = [];
let regexResult: RegExp$matchResult | null;
while ((regexResult = regex.exec(string)) !== null) {
if (regexResult != null) {
matches.push({
lowerBound: regexResult.index,
upperBound: regex.lastIndex,
});
}
}
return matches;
}
function TappableLinks(props: {
content: string,
style: void | TextStyleProp,
}): React.Node {
const matches = getLinkRanges(props.content);
if (matches.length === 0) {
// No URLs detected. Just return the content.
return <Text style={props.style}>{props.content}</Text>;
}
// URLs were detected. Construct array of Text nodes.
const fragments: Array<React.Node> = [];
let indexCounter = 0;
let startIndex = 0;
for (const linkRange of matches) {
if (startIndex < linkRange.lowerBound) {
const text = props.content.substring(startIndex, linkRange.lowerBound);
fragments.push(<Text key={++indexCounter}>{text}</Text>);
}
const link = props.content.substring(
linkRange.lowerBound,
linkRange.upperBound,
);
fragments.push(
<Text
onPress={() => {
// $FlowFixMe[unused-promise]
Linking.openURL(link);
}}
key={++indexCounter}
style={styles.linkText}>
{link}
</Text>,
);
startIndex = linkRange.upperBound;
}
if (startIndex < props.content.length) {
const text = props.content.substring(startIndex);
fragments.push(
<Text key={++indexCounter} style={props.style}>
{text}
</Text>,
);
}
return <Text style={props.style}>{fragments}</Text>;
}
const cleanContent = (content: string) =>
content.replace(/^(TransformError |Warning: (Warning: )?|Error: )/g, '');
function LogBoxMessage(props: Props): React.Node {
const {content, substitutions}: Message = props.message;
if (props.plaintext === true) {
return <Text>{cleanContent(content)}</Text>;
}
const maxLength = props.maxLength != null ? props.maxLength : Infinity;
const substitutionStyle: TextStyleProp = props.style;
const elements = [];
let length = 0;
const createUnderLength = (
key: string,
message: string,
style: void | TextStyleProp,
) => {
let cleanMessage = cleanContent(message);
if (props.maxLength != null) {
cleanMessage = cleanMessage.slice(0, props.maxLength - length);
}
if (length < maxLength) {
elements.push(
<TappableLinks content={cleanMessage} key={key} style={style} />,
);
}
length += cleanMessage.length;
};
const lastOffset = substitutions.reduce((prevOffset, substitution, index) => {
const key = String(index);
if (substitution.offset > prevOffset) {
const prevPart = content.slice(prevOffset, substitution.offset);
createUnderLength(key, prevPart);
}
const substitutionPart = content.slice(
substitution.offset,
substitution.offset + substitution.length,
);
createUnderLength(key + '.5', substitutionPart, substitutionStyle);
return substitution.offset + substitution.length;
}, 0);
if (lastOffset < content.length) {
const lastPart = content.slice(lastOffset);
createUnderLength('-1', lastPart);
}
return <>{elements}</>;
}
const styles = StyleSheet.create({
linkText: {
textDecorationLine: 'underline',
},
});
export default LogBoxMessage;

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import * as LogBoxData from '../Data/LogBoxData';
import LogBoxLog from '../Data/LogBoxLog';
import LogBoxButton from './LogBoxButton';
import LogBoxNotificationCountBadge from './LogBoxNotificationCountBadge';
import LogBoxNotificationDismissButton from './LogBoxNotificationDismissButton';
import LogBoxNotificationMessage from './LogBoxNotificationMessage';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
import {useEffect} from 'react';
type Props = $ReadOnly<{
log: LogBoxLog,
totalLogCount: number,
level: 'warn' | 'error',
onPressOpen: () => void,
onPressDismiss: () => void,
}>;
export default function LogBoxNotification(props: Props): React.Node {
const {totalLogCount, level, log} = props;
// Eagerly symbolicate so the stack is available when pressing to inspect.
useEffect(() => {
LogBoxData.symbolicateLogLazy(log);
}, [log]);
return (
<View id="logbox_notification" style={styles.container}>
<LogBoxButton
id={`logbox_open_button_${level}`}
onPress={props.onPressOpen}
style={styles.press}
backgroundColor={{
default: LogBoxStyle.getBackgroundColor(1),
pressed: LogBoxStyle.getBackgroundColor(0.9),
}}>
<View style={styles.content}>
<LogBoxNotificationCountBadge count={totalLogCount} level={level} />
<LogBoxNotificationMessage message={log.message} />
<LogBoxNotificationDismissButton
id={`logbox_dismiss_button_${level}`}
onPress={props.onPressDismiss}
/>
</View>
</LogBoxButton>
</View>
);
}
const styles = StyleSheet.create({
container: {
height: 48,
position: 'relative',
width: '100%',
justifyContent: 'center',
marginTop: 0.5,
backgroundColor: LogBoxStyle.getTextColor(1),
},
press: {
height: 48,
position: 'relative',
width: '100%',
justifyContent: 'center',
marginTop: 0.5,
paddingHorizontal: 12,
},
content: {
alignItems: 'flex-start',
flexDirection: 'row',
borderRadius: 8,
flexGrow: 0,
flexShrink: 0,
flexBasis: 'auto',
},
});

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
export default function LogBoxNotificationCountBadge(props: {
count: number,
level: 'error' | 'warn',
}): React.Node {
return (
<View style={styles.outside}>
{/* $FlowFixMe[incompatible-type] (>=0.114.0) This suppression was added
* when fixing the type of `StyleSheet.create`. Remove this comment to
* see the error. */}
<View style={[styles.inside, styles[props.level]]}>
<Text id="logbox_notification_count_text" style={styles.text}>
{props.count <= 1 ? '!' : props.count}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
warn: {
backgroundColor: LogBoxStyle.getWarningColor(1),
},
error: {
backgroundColor: LogBoxStyle.getErrorColor(1),
},
outside: {
padding: 2,
borderRadius: 25,
backgroundColor: '#fff',
marginRight: 8,
},
inside: {
minWidth: 18,
paddingLeft: 4,
paddingRight: 4,
borderRadius: 25,
fontWeight: '600',
},
text: {
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
lineHeight: 18,
textAlign: 'center',
fontWeight: '600',
textShadowColor: LogBoxStyle.getBackgroundColor(0.4),
textShadowOffset: {width: 0, height: 0},
textShadowRadius: 3,
},
});

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import View from '../../Components/View/View';
import Image from '../../Image/Image';
import StyleSheet from '../../StyleSheet/StyleSheet';
import LogBoxButton from './LogBoxButton';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
export default function LogBoxNotificationDismissButton(props: {
id: string,
onPress: () => void,
}): React.Node {
return (
<View style={styles.container}>
<LogBoxButton
id={props.id}
backgroundColor={{
default: LogBoxStyle.getTextColor(0.3),
pressed: LogBoxStyle.getTextColor(0.5),
}}
hitSlop={{
top: 12,
right: 10,
bottom: 12,
left: 10,
}}
onPress={props.onPress}
style={styles.press}>
<Image
source={require('./LogBoxImages/close.png')}
style={styles.image}
/>
</LogBoxButton>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignSelf: 'center',
flexDirection: 'row',
flexGrow: 0,
flexShrink: 0,
flexBasis: 'auto',
marginLeft: 5,
},
press: {
height: 20,
width: 20,
borderRadius: 25,
alignSelf: 'flex-end',
alignItems: 'center',
justifyContent: 'center',
},
image: {
height: 8,
width: 8,
tintColor: LogBoxStyle.getBackgroundColor(1),
},
});

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {Message as MessageType} from '../Data/parseLogBoxLog';
import View from '../../Components/View/View';
import StyleSheet from '../../StyleSheet/StyleSheet';
import Text from '../../Text/Text';
import LogBoxMessage from './LogBoxMessage';
import * as LogBoxStyle from './LogBoxStyle';
import * as React from 'react';
export default function LogBoxNotificationMessage(props: {
message: MessageType,
}): React.Node {
return (
<View style={styles.container}>
<Text
id="logbox_notification_message_text"
numberOfLines={1}
style={styles.text}>
{props.message && (
<LogBoxMessage
plaintext
message={props.message}
style={styles.substitutionText}
/>
)}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignSelf: 'stretch',
flexGrow: 1,
flexShrink: 1,
flexBasis: 'auto',
borderLeftColor: LogBoxStyle.getTextColor(0.2),
borderLeftWidth: 1,
paddingLeft: 8,
},
text: {
color: LogBoxStyle.getTextColor(1),
flex: 1,
fontSize: 14,
lineHeight: 22,
},
substitutionText: {
color: LogBoxStyle.getTextColor(0.6),
},
});

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export function getBackgroundColor(opacity?: number): string {
return `rgba(51, 51, 51, ${opacity == null ? 1 : opacity})`;
}
export function getBackgroundLightColor(opacity?: number): string {
return `rgba(69, 69, 69, ${opacity == null ? 1 : opacity})`;
}
export function getBackgroundDarkColor(opacity?: number): string {
return `rgba(34, 34, 34, ${opacity == null ? 1 : opacity})`;
}
export function getWarningColor(opacity?: number): string {
return `rgba(250, 186, 48, ${opacity == null ? 1 : opacity})`;
}
export function getWarningDarkColor(opacity?: number): string {
return `rgba(224, 167, 8, ${opacity == null ? 1 : opacity})`;
}
export function getFatalColor(opacity?: number): string {
return `rgba(243, 83, 105, ${opacity == null ? 1 : opacity})`;
}
export function getFatalDarkColor(opacity?: number): string {
return `rgba(208, 75, 95, ${opacity == null ? 1 : opacity})`;
}
export function getErrorColor(opacity?: number): string {
return `rgba(243, 83, 105, ${opacity == null ? 1 : opacity})`;
}
export function getErrorDarkColor(opacity?: number): string {
return `rgba(208, 75, 95, ${opacity == null ? 1 : opacity})`;
}
export function getLogColor(opacity?: number): string {
return `rgba(119, 119, 119, ${opacity == null ? 1 : opacity})`;
}
export function getWarningHighlightColor(opacity?: number): string {
return `rgba(252, 176, 29, ${opacity == null ? 1 : opacity})`;
}
export function getDividerColor(opacity?: number): string {
return `rgba(255, 255, 255, ${opacity == null ? 1 : opacity})`;
}
export function getHighlightColor(opacity?: number): string {
return `rgba(252, 176, 29, ${opacity == null ? 1 : opacity})`;
}
export function getTextColor(opacity?: number): string {
return `rgba(255, 255, 255, ${opacity == null ? 1 : opacity})`;
}