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,
};
}