Compare commits

...

7 Commits

Author SHA1 Message Date
Mengdi Chen
1703c68ab9 fix typo 2022-03-29 13:26:59 -04:00
Mengdi Chen
f09f0685eb fix bad import 2022-03-29 13:26:59 -04:00
Mengdi Chen
1ad075c3dc [ReactDevTools] show message for unsupported feature 2022-03-29 13:26:59 -04:00
Mengdi Chen
566e046716 [ReactDevTools] custom view for errors occur in user's code 2022-03-29 13:26:59 -04:00
Mengdi Chen
a1bec0709f [ReactDebugTools] wrap uncaught error from rendering user's component 2022-03-29 13:24:52 -04:00
Mengdi Chen
12ecbea744 update per review comments 2022-03-29 13:07:01 -04:00
Mengdi Chen
60cb32bf93 [ReactDebugTools] add custom error type for future new hooks 2022-03-28 12:09:56 -04:00
11 changed files with 260 additions and 10 deletions

View File

@@ -356,6 +356,23 @@ const Dispatcher: DispatcherType = {
useId, useId,
}; };
// create a proxy to throw a custom error
// in case future versions of React adds more hooks
const DispatcherProxyHandler = {
get(target, prop, _receiver) {
if (target.hasOwnProperty(prop)) {
return target[prop];
}
const error = new Error('Missing method in Dispatcher: ' + prop);
// Note: This error name needs to stay in sync with react-devtools-shared
// TODO: refactor this if we ever combine the devtools and debug tools packages
error.name = 'UnsupportedFeatureError';
throw error;
},
};
const DispatcherProxy = new Proxy(Dispatcher, DispatcherProxyHandler);
// Inspect // Inspect
export type HookSource = { export type HookSource = {
@@ -650,6 +667,28 @@ function processDebugValues(
} }
} }
function handleRenderFunctionError(error: any): void {
// original error might be any type.
const isError = error instanceof Error;
if (isError && error.name === 'UnsupportedFeatureError') {
throw error;
}
// If the error is not caused by an unsupported feature, it means
// that the error is caused by user's code in renderFunction.
// In this case, we should wrap the original error inside a custom error
// so that devtools can show a clear message for it.
const messgae: string =
isError && error.message
? error.message
: 'Error rendering inspected component'
// $FlowFixMe: Flow doesn't know about 2nd argument of Error constructor
const wrapperError = new Error(messgae, {cause: error});
// Note: This error name needs to stay in sync with react-devtools-shared
// TODO: refactor this if we ever combine the devtools and debug tools packages
wrapperError.name = 'RenderFunctionError';
throw wrapperError;
}
export function inspectHooks<Props>( export function inspectHooks<Props>(
renderFunction: Props => React$Node, renderFunction: Props => React$Node,
props: Props, props: Props,
@@ -664,11 +703,13 @@ export function inspectHooks<Props>(
const previousDispatcher = currentDispatcher.current; const previousDispatcher = currentDispatcher.current;
let readHookLog; let readHookLog;
currentDispatcher.current = Dispatcher; currentDispatcher.current = DispatcherProxy;
let ancestorStackError; let ancestorStackError;
try { try {
ancestorStackError = new Error(); ancestorStackError = new Error();
renderFunction(props); renderFunction(props);
} catch (error) {
handleRenderFunctionError(error);
} finally { } finally {
readHookLog = hookLog; readHookLog = hookLog;
hookLog = []; hookLog = [];
@@ -708,11 +749,13 @@ function inspectHooksOfForwardRef<Props, Ref>(
): HooksTree { ): HooksTree {
const previousDispatcher = currentDispatcher.current; const previousDispatcher = currentDispatcher.current;
let readHookLog; let readHookLog;
currentDispatcher.current = Dispatcher; currentDispatcher.current = DispatcherProxy;
let ancestorStackError; let ancestorStackError;
try { try {
ancestorStackError = new Error(); ancestorStackError = new Error();
renderFunction(props, ref); renderFunction(props, ref);
} catch (error) {
handleRenderFunctionError(error);
} finally { } finally {
readHookLog = hookLog; readHookLog = hookLog;
hookLog = []; hookLog = [];

View File

@@ -60,7 +60,10 @@ import {
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION, TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
} from '../constants'; } from '../constants';
import {inspectHooksOfFiber} from 'react-debug-tools'; import {
inspectHooksOfFiber,
ErrorsNames as DebugToolsErrors,
} from 'react-debug-tools';
import { import {
patch as patchConsole, patch as patchConsole,
registerRenderer as registerRendererWithConsole, registerRenderer as registerRendererWithConsole,
@@ -3616,6 +3619,41 @@ export function attach(
try { try {
mostRecentlyInspectedElement = inspectElementRaw(id); mostRecentlyInspectedElement = inspectElementRaw(id);
} catch (error) { } catch (error) {
if (error.name === DebugToolsErrors.RENDER_FUNCTION_ERROR) {
let message = 'Error rendering inspected element.';
let stack;
// Log error & cause for user to debug
console.error(message + '\n\n', error);
if (error.cause != null) {
console.error(
'Original error causing above error: \n\n',
error.cause,
);
if (error.cause instanceof Error) {
message = error.cause.message || message;
stack = error.cause.stack;
}
}
return {
type: 'user-error',
id,
responseID: requestID,
message,
stack,
};
}
if (error.name === DebugToolsErrors.UNSUPPORTTED_FEATURE_ERROR) {
return {
type: 'unsupported-feature',
id,
responseID: requestID,
message: 'Unsupported feature: ' + error.message,
};
}
// Log Uncaught Error
console.error('Error inspecting element.\n\n', error); console.error('Error inspecting element.\n\n', error);
return { return {

View File

@@ -281,6 +281,8 @@ export type InspectedElement = {|
|}; |};
export const InspectElementErrorType = 'error'; export const InspectElementErrorType = 'error';
export const InspectElementUserErrorType = 'user-error';
export const InspectElementUnsupportedFeatureErrorType = 'unsupported-feature';
export const InspectElementFullDataType = 'full-data'; export const InspectElementFullDataType = 'full-data';
export const InspectElementNoChangeType = 'no-change'; export const InspectElementNoChangeType = 'no-change';
export const InspectElementNotFoundType = 'not-found'; export const InspectElementNotFoundType = 'not-found';
@@ -293,6 +295,21 @@ export type InspectElementError = {|
stack: string, stack: string,
|}; |};
export type InspectElementUserError = {|
id: number,
responseID: number,
type: 'user-error',
message: string,
stack: ?string,
|};
export type InspectElementUnsupportedFeatureError = {|
id: number,
responseID: number,
type: 'unsupported-feature',
message: string,
|};
export type InspectElementFullData = {| export type InspectElementFullData = {|
id: number, id: number,
responseID: number, responseID: number,
@@ -322,6 +339,8 @@ export type InspectElementNotFound = {|
export type InspectedElementPayload = export type InspectedElementPayload =
| InspectElementError | InspectElementError
| InspectElementUserError
| InspectElementUnsupportedFeatureError
| InspectElementFullData | InspectElementFullData
| InspectElementHydratedPath | InspectElementHydratedPath
| InspectElementNoChange | InspectElementNoChange

View File

@@ -10,7 +10,7 @@
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration'; import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils'; import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
import Store from 'react-devtools-shared/src/devtools/store'; import Store from 'react-devtools-shared/src/devtools/store';
import TimeoutError from 'react-devtools-shared/src/TimeoutError'; import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
import type { import type {
InspectedElement as InspectedElementBackend, InspectedElement as InspectedElementBackend,

View File

@@ -63,6 +63,7 @@ export type OwnersList = {|
export type InspectedElementResponseType = export type InspectedElementResponseType =
| 'error' | 'error'
| 'user-error'
| 'full-data' | 'full-data'
| 'hydrated-path' | 'hydrated-path'
| 'no-change' | 'no-change'

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import styles from './shared.css';
type Props = {|
callStack: string | null,
children: React$Node,
info: React$Node | null,
componentStack: string | null,
errorMessage: string,
|};
export default function CaughtErrorView({
callStack,
children,
info,
componentStack,
errorMessage,
}: Props) {
return (
<div className={styles.ErrorBoundary}>
{children}
<div className={styles.ErrorInfo}>
<div className={styles.HeaderRow}>
<div className={styles.ErrorHeader}>{errorMessage}</div>
</div>
{!!info && <div className={styles.InfoBox}>{info}</div>}
{!!callStack && (
<div className={styles.ErrorStack}>
The error was thrown {callStack.trim()}
</div>
)}
</div>
</div>
);
}

View File

@@ -15,8 +15,11 @@ import ErrorView from './ErrorView';
import SearchingGitHubIssues from './SearchingGitHubIssues'; import SearchingGitHubIssues from './SearchingGitHubIssues';
import SuspendingErrorView from './SuspendingErrorView'; import SuspendingErrorView from './SuspendingErrorView';
import TimeoutView from './TimeoutView'; import TimeoutView from './TimeoutView';
import CaughtErrorView from './CaughtErrorView';
import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError'; import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError';
import TimeoutError from 'react-devtools-shared/src/TimeoutError'; import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
import UserError from 'react-devtools-shared/src/errors/UserError';
import UnsupportedFeatureError from 'react-devtools-shared/src/errors/UnsupportedFeatureError';
import {logEvent} from 'react-devtools-shared/src/Logger'; import {logEvent} from 'react-devtools-shared/src/Logger';
type Props = {| type Props = {|
@@ -34,6 +37,8 @@ type State = {|
hasError: boolean, hasError: boolean,
isUnsupportedBridgeOperationError: boolean, isUnsupportedBridgeOperationError: boolean,
isTimeout: boolean, isTimeout: boolean,
isUserError: boolean,
isUnsupportedFeatureError: boolean,
|}; |};
const InitialState: State = { const InitialState: State = {
@@ -44,6 +49,8 @@ const InitialState: State = {
hasError: false, hasError: false,
isUnsupportedBridgeOperationError: false, isUnsupportedBridgeOperationError: false,
isTimeout: false, isTimeout: false,
isUserError: false,
isUnsupportedFeatureError: false,
}; };
export default class ErrorBoundary extends Component<Props, State> { export default class ErrorBoundary extends Component<Props, State> {
@@ -58,6 +65,8 @@ export default class ErrorBoundary extends Component<Props, State> {
: null; : null;
const isTimeout = error instanceof TimeoutError; const isTimeout = error instanceof TimeoutError;
const isUserError = error instanceof UserError;
const isUnsupportedFeatureError = error instanceof UnsupportedFeatureError;
const isUnsupportedBridgeOperationError = const isUnsupportedBridgeOperationError =
error instanceof UnsupportedBridgeOperationError; error instanceof UnsupportedBridgeOperationError;
@@ -76,7 +85,9 @@ export default class ErrorBoundary extends Component<Props, State> {
errorMessage, errorMessage,
hasError: true, hasError: true,
isUnsupportedBridgeOperationError, isUnsupportedBridgeOperationError,
isUnsupportedFeatureError,
isTimeout, isTimeout,
isUserError,
}; };
} }
@@ -111,6 +122,8 @@ export default class ErrorBoundary extends Component<Props, State> {
hasError, hasError,
isUnsupportedBridgeOperationError, isUnsupportedBridgeOperationError,
isTimeout, isTimeout,
isUserError,
isUnsupportedFeatureError,
} = this.state; } = this.state;
if (hasError) { if (hasError) {
@@ -133,6 +146,38 @@ export default class ErrorBoundary extends Component<Props, State> {
errorMessage={errorMessage} errorMessage={errorMessage}
/> />
); );
} else if (isUserError) {
return (
<CaughtErrorView
callStack={callStack}
componentStack={componentStack}
errorMessage={errorMessage || 'Error occured in inspected element'}
info={
<>
This is likely to be caused by implementation of current
inspected element. Please see your console for logged error.
</>
}
/>
);
} else if (isUnsupportedFeatureError) {
return (
<CaughtErrorView
callStack={callStack}
componentStack={componentStack}
errorMessage={
errorMessage ||
'Current DevTools version does not support a feature used in the inspected element.'
}
info={
<>
React DevTools is unable to handle a feature you are using in
this component (e.g. a new React build-in Hook). Please upgrade
to the latest version.
</>
}
/>
);
} else { } else {
return ( return (
<ErrorView <ErrorView
@@ -141,10 +186,7 @@ export default class ErrorBoundary extends Component<Props, State> {
dismissError={ dismissError={
canDismissProp || canDismissState ? this._dismissError : null canDismissProp || canDismissState ? this._dismissError : null
} }
errorMessage={errorMessage} errorMessage={errorMessage}>
isUnsupportedBridgeOperationError={
isUnsupportedBridgeOperationError
}>
<Suspense fallback={<SearchingGitHubIssues />}> <Suspense fallback={<SearchingGitHubIssues />}>
<SuspendingErrorView <SuspendingErrorView
callStack={callStack} callStack={callStack}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export default class UnsupportedFeatureError extends Error {
constructor(message: string) {
super(message);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, UnsupportedFeatureError);
}
this.name = 'UnsupportedFeatureError';
}
}

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export default class UserError extends Error {
constructor(message: string) {
super(message);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, UserError);
}
this.name = 'UserError';
}
}

View File

@@ -8,6 +8,7 @@
*/ */
import LRU from 'lru-cache'; import LRU from 'lru-cache';
import {UserHookError} from 'react-debug-tools';
import { import {
convertInspectedElementBackendToFrontend, convertInspectedElementBackendToFrontend,
hydrateHelper, hydrateHelper,
@@ -19,6 +20,8 @@ import type {LRUCache} from 'react-devtools-shared/src/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type { import type {
InspectElementError, InspectElementError,
InspectElementUserError,
InspectElementUnsupportedFeatureError,
InspectElementFullData, InspectElementFullData,
InspectElementHydratedPath, InspectElementHydratedPath,
} from 'react-devtools-shared/src/backend/types'; } from 'react-devtools-shared/src/backend/types';
@@ -27,6 +30,8 @@ import type {
InspectedElement as InspectedElementFrontend, InspectedElement as InspectedElementFrontend,
InspectedElementResponseType, InspectedElementResponseType,
} from 'react-devtools-shared/src/devtools/views/Components/types'; } from 'react-devtools-shared/src/devtools/views/Components/types';
import UserError from 'react-devtools-shared/src/errors/UserError';
import UnsupportedFeatureError from 'react-devtools-shared/src/errors/UnsupportedFeatureError';
// Maps element ID to inspected data. // Maps element ID to inspected data.
// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works. // We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
@@ -80,7 +85,7 @@ export function inspectElement({
let inspectedElement; let inspectedElement;
switch (type) { switch (type) {
case 'error': case 'error': {
const {message, stack} = ((data: any): InspectElementError); const {message, stack} = ((data: any): InspectElementError);
// The backend's stack (where the error originated) is more meaningful than this stack. // The backend's stack (where the error originated) is more meaningful than this stack.
@@ -88,6 +93,22 @@ export function inspectElement({
error.stack = stack; error.stack = stack;
throw error; throw error;
}
case 'user-error': {
const {message, stack} = (data: InspectElementUserError);
// Trying to keep useful information from user's side.
const error = new UserError(message);
error.stack = stack || error.stack;
throw error;
}
case 'unsupported-feature': {
const {message} = (data: InspectElementUnsupportedFeatureError);
// Trying to keep useful information from user's side.
const error = new UnsupportedFeatureError(message);
throw error;
}
case 'no-change': case 'no-change':
// This is a no-op for the purposes of our cache. // This is a no-op for the purposes of our cache.