FRE-600: Fix code review blockers

- Consolidated duplicate UndoManagers to single instance
- Fixed connection promise to only resolve on 'connected' status
- Fixed WebSocketProvider import (WebsocketProvider)
- Added proper doc.destroy() cleanup
- Renamed isPresenceInitialized property to avoid conflict

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-25 00:08:01 -04:00
parent 65b552bb08
commit 7c684a42cc
48450 changed files with 5679671 additions and 383 deletions

View File

@@ -0,0 +1,558 @@
/**
* 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 {Stack} from './LogBoxSymbolication';
import type {Category, ExtendedExceptionData, Message} from './parseLogBoxLog';
import DebuggerSessionObserver from '../../../src/private/devsupport/rndevtools/FuseboxSessionObserver';
import TracingStateObserver from '../../../src/private/devsupport/rndevtools/TracingStateObserver';
import toExtendedError from '../../../src/private/utilities/toExtendedError';
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: Stack,
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 tracingStateObserverSubscription = 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 (tracingStateObserverSubscription == null) {
tracingStateObserverSubscription = TracingStateObserver.subscribe(
isTracing => {
if (isTracing) {
clear();
}
},
);
}
if (TracingStateObserver.isTracing()) {
return;
}
// 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,
}),
);
} catch (error: unknown) {
reportLogBoxError(toExtendedError(error));
}
});
}
export function addException(error: ExtendedExceptionData): void {
if (TracingStateObserver.isTracing()) {
return;
}
// Parsing logs are expensive so we schedule this
// otherwise spammy logs would pause rendering.
setImmediate(() => {
try {
appendNewLog(new LogBoxLog(parseLogBoxException(error)));
} catch (loggingError: unknown) {
reportLogBoxError(toExtendedError(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);
},
};
}
/**
* Same as observe(), but doesn't call notify observer sync at the time of subscription.
* Expected to be used only in LogBoxStateSubscription.
*/
function observeNext(observer: Observer): Subscription {
const subscription = {observer};
observers.add(subscription);
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;
_updateStateOnMountTimeoutId: ?TimeoutID;
state: LogBoxStateSubscriptionState = {
hasError: false,
...getNextState(),
};
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 = observeNext(data => {
this.setState(data);
});
/**
* This should cover the case when the state changes in between the first render and mount effect.
* We defer the state update to next task to avoid cascading update.
*/
this._updateStateOnMountTimeoutId = setTimeout(() => {
this._updateStateOnMountTimeoutId = null;
this.setState(getNextState());
}, 0);
}
componentWillUnmount(): void {
if (this._updateStateOnMountTimeoutId != null) {
clearTimeout(this._updateStateOnMountTimeoutId);
}
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,210 @@
/**
* 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, Message} from './parseLogBoxLog';
import * as LogBoxSymbolication from './LogBoxSymbolication';
type SymbolicationStatus = 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED';
type SymbolicationState =
| 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'}>;
export type LogLevel = 'warn' | 'error' | 'fatal' | 'syntax';
export type LogBoxLogData = Readonly<{
level: LogLevel,
type?: ?string,
message: Message,
stack: Stack,
category: string,
componentStack: Stack,
codeFrame?: ?CodeFrame,
isComponentError: boolean,
extraData?: unknown,
onNotificationPress?: ?() => void,
}>;
class LogBoxLog {
message: Message;
type: ?string;
category: Category;
componentStack: Stack;
stack: Stack;
count: number;
level: LogLevel;
codeFrame: ?CodeFrame;
componentCodeFrame: ?CodeFrame;
isComponentError: boolean;
extraData: unknown | void;
symbolicated: SymbolicationState = {
error: null,
stack: null,
status: 'NONE',
};
symbolicatedComponentStack: SymbolicationState = {
error: null,
stack: 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.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(): Stack {
return this.symbolicatedComponentStack.status === 'COMPLETE'
? this.symbolicatedComponentStack.stack
: 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(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.componentStack.length > 0 &&
this.symbolicatedComponentStack.status !== 'PENDING' &&
this.symbolicatedComponentStack.status !== 'COMPLETE'
) {
this.updateComponentStackStatus(null, null, null, callback);
LogBoxSymbolication.symbolicate(this.componentStack, []).then(
data => {
this.updateComponentStackStatus(
null,
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;
this.symbolicated = this._computeSymbolicationState(error, stack);
if (stack != null && codeFrame) {
this.codeFrame = codeFrame;
}
if (callback && lastStatus !== this.symbolicated.status) {
callback(this.symbolicated.status);
}
}
updateComponentStackStatus(
error: ?Error,
stack: ?Stack,
codeFrame: ?CodeFrame,
callback?: (status: SymbolicationStatus) => void,
): void {
const lastStatus = this.symbolicatedComponentStack.status;
this.symbolicatedComponentStack = this._computeSymbolicationState(
error,
stack,
);
if (stack != null && codeFrame) {
this.componentCodeFrame = codeFrame;
}
if (callback && lastStatus !== this.symbolicatedComponentStack.status) {
callback(this.symbolicatedComponentStack.status);
}
}
_computeSymbolicationState(error: ?Error, stack: ?Stack): SymbolicationState {
if (error != null) {
return {
error,
stack: null,
status: 'FAILED',
};
} else if (stack != null) {
return {
error: null,
stack,
status: 'COMPLETE',
};
} else {
return {
error: null,
stack: null,
status: 'PENDING',
};
}
}
}
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?: unknown,
): 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,420 @@
/**
* 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 type {Stack} from './LogBoxSymbolication';
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}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:
// - 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 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,
}>,
>,
}>;
const SUBSTITUTION = UTFSequence.BOM + '%s';
export function parseInterpolation(args: ReadonlyArray<unknown>): 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) {
// Component stacks are formatted as call stack frames:
// - Hermes format: " at Component (/path/to/file.js:1:2)"
// - JSC format: "Component@/path/to/file.js:1:2"
return (
RE_COMPONENT_STACK_LINE.test(consoleArgument) ||
RE_COMPONENT_STACK_LINE_STACK_FRAME.test(consoleArgument)
);
}
export function parseComponentStack(message: string): {
stack: Stack,
} {
const stack = parseErrorStack(message);
return {
stack: stack ?? [],
};
}
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,
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,
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,
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,
componentStack: [],
message: {
content: message,
substitutions: [],
},
category: message,
extraData: error.extraData,
};
}
const componentStack = error.componentStack;
if (error.isFatal || error.isComponentError) {
if (componentStack != null) {
const {stack} = parseComponentStack(componentStack);
return {
level: 'fatal',
stack: error.stack,
isComponentError: error.isComponentError,
componentStack: stack,
extraData: error.extraData,
...parseInterpolation([message]),
};
} else {
return {
level: 'fatal',
stack: error.stack,
isComponentError: error.isComponentError,
componentStack: [],
extraData: error.extraData,
...parseInterpolation([message]),
};
}
}
if (componentStack != null) {
// It is possible that console errors have a componentStack.
const {stack} = parseComponentStack(componentStack);
return {
level: 'error',
stack: error.stack,
isComponentError: error.isComponentError,
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: unknown): unknown {
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<unknown>): {
componentStack: Stack,
category: Category,
message: Message,
} {
const message = withoutANSIColorStyles(args[0]);
let argsWithoutComponentStack: Array<unknown> = [];
let componentStack: Stack = [];
// 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 {stack} = parseComponentStack(lastArg);
componentStack = stack;
}
}
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 {stack} = parseComponentStack(arg);
componentStack = stack;
} else {
argsWithoutComponentStack.push(arg);
}
}
}
return {
...parseInterpolation(argsWithoutComponentStack),
componentStack,
};
}

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;

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

@@ -0,0 +1,307 @@
/**
* 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 {Stack} from './Data/LogBoxSymbolication';
import type {ExtendedExceptionData} from './Data/parseLogBoxLog';
import toExtendedError from '../../src/private/utilities/toExtendedError';
import Platform from '../Utilities/Platform';
import RCTLog from '../Utilities/RCTLog';
import * as React from 'react';
// TODO: Remove support for LegacyComponentStackFrame in a future version.
// This is kept for backward compatibility with external callers of LogBox.addLog.
function convertLegacyComponentStack(componentStack: Stack): Stack {
if (componentStack.length === 0) {
return [];
}
// Detect legacy format by checking for 'content' property
const firstFrame = componentStack[0];
if (
firstFrame != null &&
typeof firstFrame === 'object' &&
// $FlowExpectedError[prop-missing]
typeof firstFrame.content === 'string'
) {
// Convert from legacy ComponentStack to Stack format
return (componentStack: $FlowFixMe).map(frame => ({
methodName: frame.content,
lineNumber: frame.location.row,
column: frame.location.column,
file: frame.fileName,
collapse: frame.collapse ?? false,
}));
}
// Already in the new Stack format
return componentStack;
}
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<unknown>): 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<unknown>) => void;
let isLogBoxInstalled: boolean = false;
LogBox = {
install(): void {
if (isLogBoxInstalled) {
return;
}
isLogBoxInstalled = true;
if (global.RN$registerExceptionListener != null) {
global.RN$registerExceptionListener(
(error: ExtendedExceptionData & {preventDefault: () => unknown}) => {
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,
componentStack: convertLegacyComponentStack(log.componentStack),
});
}
},
addConsoleLog(level: 'warn' | 'error', ...args: Array<unknown>) {
if (isLogBoxInstalled) {
let filteredLevel: 'warn' | 'error' | 'fatal' = level;
try {
let format = args[0];
if (typeof format === 'string') {
const filterResult =
require('../LogBox/Data/LogBoxData').checkWarningFilter(format);
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 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;
}
}
if (!LogBoxData.isMessageIgnored(message.content)) {
LogBoxData.addLog({
level: filteredLevel,
category,
message,
componentStack,
});
}
} catch (err: unknown) {
LogBoxData.reportLogBoxError(toExtendedError(err));
}
}
},
addException,
};
function addException(error: ExtendedExceptionData): void {
if (isLogBoxInstalled) {
LogBoxData.addException(error);
}
}
const isRCTLogAdviceWarning = (...args: Array<unknown>) => {
// 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<unknown>): 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} = parseLogBoxLog(args);
if (!LogBoxData.isMessageIgnored(message.content)) {
LogBoxData.addLog({
level: 'warn',
category,
message,
componentStack,
});
}
}
} catch (err: unknown) {
LogBoxData.reportLogBoxError(toExtendedError(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<unknown>): 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,71 @@
/**
* 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';
component LogBoxButton(
id?: string,
backgroundColor: Readonly<{
default: string,
pressed: string,
}>,
children?: React.Node,
hitSlop?: ?EdgeInsetsProp,
onPress?: ?(event: GestureResponderEvent) => void,
style?: ViewStyleProp,
) {
const [pressed, setPressed] = useState(false);
let resolvedBackgroundColor = backgroundColor;
if (!resolvedBackgroundColor) {
resolvedBackgroundColor = {
default: LogBoxStyle.getBackgroundColor(0.95),
pressed: LogBoxStyle.getBackgroundColor(0.6),
};
}
const content = (
<View
id={id}
style={StyleSheet.compose(
{
backgroundColor: pressed
? resolvedBackgroundColor.pressed
: resolvedBackgroundColor.default,
},
style,
)}>
{children}
</View>
);
return onPress == null ? (
content
) : (
<TouchableWithoutFeedback
hitSlop={hitSlop}
onPress={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,180 @@
/**
* 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';
component CodeFrameDisplay(codeFrame: CodeFrame) {
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>
);
}
component LogBoxInspectorCodeFrame(
componentCodeFrame: ?CodeFrame,
codeFrame: ?CodeFrame,
) {
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>
);
}
component 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,122 @@
/**
* 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';
const SHOW_MORE_MESSAGE_LENGTH = 300;
component LogBoxInspectorMessageHeader(
collapsed: boolean,
message: Message,
level: LogLevel,
title: string,
onPress: () => void,
) {
function renderShowMore() {
if (message.content.length < SHOW_MORE_MESSAGE_LENGTH || !collapsed) {
return null;
}
return (
<Text style={messageStyles.collapse} onPress={() => onPress()}>
... See More
</Text>
);
}
return (
<View style={messageStyles.body}>
<View style={messageStyles.heading}>
<Text
style={[messageStyles.headingText, messageStyles[level]]}
id="logbox_message_title_text">
{title}
</Text>
</View>
<Text style={messageStyles.bodyText} id="logbox_message_contents_text">
<LogBoxMessage
maxLength={collapsed ? SHOW_MORE_MESSAGE_LENGTH : Infinity}
message={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,198 @@
/**
* 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';
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;
}
component LogBoxInspectorReactFrames(log: LogBoxLog) {
const [collapsed, setCollapsed] = useState(true);
if (
log.getAvailableComponentStack() == null ||
log.getAvailableComponentStack().length < 1
) {
return null;
}
function getStackList() {
if (collapsed) {
return log.getAvailableComponentStack().slice(0, 3);
} else {
return log.getAvailableComponentStack();
}
}
function getCollapseMessage() {
if (log.getAvailableComponentStack().length <= 3) {
return;
}
const count = 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.file != null && frame.file.startsWith('/')
? () =>
openFileInEditor(frame.file ?? '', frame.lineNumber ?? 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.methodName}
<Text style={componentStyles.bracket}>{' />'}</Text>
</Text>
</View>
<Text style={componentStyles.frameLocation}>
{frame.file != null ? getPrettyFileName(frame.file) : ''}
{frame.lineNumber != null ? `:${frame.lineNumber}` : ''}
</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',
macos: 'Menlo',
windows: 'Consolas',
}),
color: LogBoxStyle.getTextColor(1),
fontSize: 14,
includeFontPadding: false,
lineHeight: 18,
},
bracket: {
fontFamily: Platform.select({
android: 'monospace',
ios: 'Menlo',
macos: 'Menlo',
windows: 'Consolas',
}),
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,56 @@
/**
* 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';
component LogBoxInspectorSection(
heading: string,
children: React.Node,
action?: ?React.Node,
) {
return (
<View style={styles.section}>
<View style={styles.heading}>
<Text style={styles.headingText}>{heading}</Text>
{action}
</View>
<View style={styles.body}>{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,132 @@
/**
* 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';
component LogBoxInspectorSourceMapStatus(
onPress?: ?(event: GestureResponderEvent) => void,
status: 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
) {
const [state, setState] = useState({
animation: null,
rotate: null,
});
useEffect(() => {
if (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();
}
};
}, [status, state.animation]);
let image;
let color;
switch (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 (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={onPress}
style={styles.root}>
<Animated.Image
source={image}
style={[
styles.image,
{tintColor: color},
state.rotate == null || 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,118 @@
/**
* 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';
component LogBoxInspectorStackFrame(
frame: StackFrame,
onPress?: ?(event: GestureResponderEvent) => void,
) {
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',
macos: 'Menlo',
windows: 'Consolas',
}),
},
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,210 @@
/**
* 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';
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}`;
}
}
component LogBoxInspectorStackFrames(log: LogBoxLog, onRetry: () => void) {
const [collapsed, setCollapsed] = useState(() => {
// Only collapse frames initially if some frames are not collapsed.
return log.getAvailableStack().some(({collapse}) => !collapse);
});
function getStackList() {
if (collapsed === true) {
return log.getAvailableStack().filter(({collapse}) => !collapse);
} else {
return log.getAvailableStack();
}
}
if (log.getAvailableStack().length === 0) {
return null;
}
return (
<LogBoxInspectorSection
heading="Call Stack"
action={
<LogBoxInspectorSourceMapStatus
onPress={log.symbolicated.status === 'FAILED' ? onRetry : null}
status={log.symbolicated.status}
/>
}>
{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={log.symbolicated.status} />
<StackFrameFooter
onPress={() => setCollapsed(!collapsed)}
message={getCollapseMessage(log.getAvailableStack(), collapsed)}
/>
</LogBoxInspectorSection>
);
}
component StackFrameList(
list: Stack | Array<StackFrame>,
status: string | 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING',
) {
return (
<>
{list.map((frame, index) => {
const {file, lineNumber} = frame;
return (
<LogBoxInspectorStackFrame
key={index}
frame={frame}
onPress={
status === 'COMPLETE' && file != null && lineNumber != null
? () => openFileInEditor(file, lineNumber)
: null
}
/>
);
})}
</>
);
}
component StackFrameFooter(message: string, onPress: () => void) {
return (
<View style={stackStyles.collapseContainer}>
<LogBoxButton
backgroundColor={{
default: 'transparent',
pressed: LogBoxStyle.getBackgroundColor(1),
}}
onPress={onPress}
style={stackStyles.collapseButton}>
<Text style={stackStyles.collapse}>{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,160 @@
/**
* 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 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;
}
component TappableLinks(content: string, style: void | TextStyleProp) {
const matches = getLinkRanges(content);
if (matches.length === 0) {
// No URLs detected. Just return the content.
return <Text style={style}>{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 = content.substring(startIndex, linkRange.lowerBound);
fragments.push(<Text key={++indexCounter}>{text}</Text>);
}
const link = 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 < content.length) {
const text = content.substring(startIndex);
fragments.push(
<Text key={++indexCounter} style={style}>
{text}
</Text>,
);
}
return <Text style={style}>{fragments}</Text>;
}
const cleanContent = (content: string) =>
content.replace(/^(TransformError |Error: )/g, '');
component LogBoxMessage(
message: Message,
style: TextStyleProp,
plaintext?: ?boolean,
maxLength?: ?number,
) {
const {content, substitutions}: Message = message;
if (plaintext === true) {
return <Text>{cleanContent(content)}</Text>;
}
const resolvedMaxLength = maxLength != null ? maxLength : Infinity;
const substitutionStyle: TextStyleProp = style;
const elements = [];
let length = 0;
const createUnderLength = (
key: string,
messageText: string,
textStyle: void | TextStyleProp,
) => {
let cleanMessage = cleanContent(messageText);
if (maxLength != null) {
cleanMessage = cleanMessage.slice(0, maxLength - length);
}
if (length < resolvedMaxLength) {
elements.push(
<TappableLinks content={cleanMessage} key={key} style={textStyle} />,
);
}
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})`;
}