Compare commits

..

3 Commits

Author SHA1 Message Date
Joe Savona
4f05deca4e [compiler] Improve merging of scopes that invalidate together
We try to merge consecutive reactive scopes that will always invalidate together, but there's one common case that isn't handled.

```js
const y = [[x]];
```

Here we'll create two consecutive scopes for the inner and outer array expressions. Because the input to the second scope is a temporary, they'll merge into one scope.

But if we name the inner array, the merging stops:

```js
const array = [x];
const y = [array];
```

This is because the merging logic checks if all the dependencies of the second scope are outputs of the first scope, but doesn't account for renaming due to LoadLocal/StoreLocal. The fix is to track these temporaries.
2025-07-29 21:59:00 -07:00
Joe Savona
75be876f2a [compiler] Add definitions for Object entries/keys/values
Fixes remaining issue in #32261, where passing a previously useMemo()-d value to `Object.entries()` makes the compiler think the value is mutated and fail validatePreserveExistingMemo. While I was there I added Object.keys() and Object.values() too.
2025-07-29 21:58:58 -07:00
Joe Savona
7d696dc3b8 Enable ref validation in linter 2025-07-29 12:22:05 -07:00
61 changed files with 576 additions and 5662 deletions

View File

@@ -92,6 +92,7 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -105,7 +106,6 @@ import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -292,10 +292,6 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
@@ -326,6 +322,13 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));

View File

@@ -1,752 +0,0 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -1,131 +0,0 @@
import {CompilerError, SourceLocation} from '..';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
loc,
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -1,312 +0,0 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

View File

@@ -49,7 +49,6 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -244,12 +243,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
*/
flowTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* Enable a new model for mutability and aliasing inference
*/
@@ -330,12 +323,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -704,8 +691,6 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -774,26 +759,6 @@ export class Environment {
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
loc: null,
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
loc: null,
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {

View File

@@ -14,7 +14,6 @@ import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
/*
* *******************************************************************************************
@@ -1321,21 +1320,12 @@ export function forkTemporaryIdentifier(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
}
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
return {
kind: 'named',
value: name as ValidIdentifierName,

View File

@@ -504,7 +504,7 @@ function canMergeScopes(
return false;
}
export function isAlwaysInvalidatingType(type: Type): boolean {
function isAlwaysInvalidatingType(type: Type): boolean {
switch (type.kind) {
case 'Object': {
switch (type.shapeId) {

View File

@@ -0,0 +1,110 @@
/**
* 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.
*/
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
/**
* Temporary workaround for InferTypes not propagating the types of phis.
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
* the phi itself) with a single "canonical" identifier, generally chosen as the first
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
* sometimes be an operand from the earlier phi.
*
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
* could end up replacing the phi Identifier with another identifer from an operand,
* which _did_ have a type inferred.
*
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
* will always invalidate with its input.
*
* The long-term correct approach is to update InferTypes to infer the types of phis,
* but this is complicated because InferMutableRanges inadvertently depends on phis
* never having a known type, such that a Store effect cannot occur on a phi value.
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
* to handle this case.
*
* As a temporary workaround, this pass propagates the type of phis and can be called
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
* type if all operands have the same type, it's its more correct.
*/
export function propagatePhiTypes(fn: HIRFunction): void {
/**
* We track which SSA ids have had their types propagated to handle nested ternaries,
* see the StoreLocal handling below
*/
const propagated = new Set<IdentifierId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
/*
* We replicate the previous LeaveSSA behavior and only propagate types for
* unnamed variables. LeaveSSA would have chosen one of the operands as the
* canonical id and taken its type as the type of all identifiers. We're
* more conservative and only propagate if the types are the same and the
* phi didn't have a type inferred.
*
* Note that this can change output slightly in cases such as
* `cond ? <div /> : null`.
*
* Previously the first operand's type (BuiltInJsx) would have been propagated,
* and this expression may have been merged with subsequent reactive scopes
* since it appears (based on that type) to always invalidate.
*
* But the correct type is `BuiltInJsx | null`, which we can't express and
* so leave as a generic `Type`, which does not always invalidate and therefore
* does not merge with subsequent scopes.
*
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'StoreLocal': {
/**
* Nested ternaries can lower to a form with an intermediate StoreLocal where
* the value.lvalue is the temporary of the outer ternary, and the value.value
* is the result of the inner ternary.
*
* This is a common pattern in practice and easy enough to support. Again, the
* long-term approach is to update InferTypes and InferMutableRanges.
*/
const lvalue = value.lvalue.place;
if (
propagated.has(value.value.identifier.id) &&
lvalue.identifier.type.kind === 'Type' &&
lvalue.identifier.name === null
) {
lvalue.identifier.type = value.value.identifier.type;
propagated.add(lvalue.identifier.id);
}
}
}
}
}
}

View File

@@ -78,10 +78,6 @@ export default class DisjointSet<T> {
return root;
}
has(item: T): boolean {
return this.#entries.has(item);
}
/*
* Forces the set into canonical form, ie with all items pointing directly to
* their root, and returns a Map representing the mapping of items to their roots.

View File

@@ -1,87 +0,0 @@
/**
* 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.
*/
/**
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
*/
/**
* Note: `await` and `yield` are contextually allowed as identifiers.
* await: reserved inside async functions and modules
* yield: reserved inside generator functions
*
* Note: `async` is not reserved.
*/
const RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
]);
/**
* Reserved when a module has a 'use strict' directive.
*/
const STRICT_MODE_RESERVED_WORDS = new Set([
'let',
'static',
'implements',
'interface',
'package',
'private',
'protected',
'public',
]);
/**
* The names arguments and eval are not keywords, but they are subject to some restrictions in
* strict mode code.
*/
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
/**
* Conservative check for whether an identifer name is reserved or not. We assume that code is
* written with strict mode.
*/
export function isReservedWord(identifierName: string): boolean {
return (
RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
);
}

View File

@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T, index: number) => boolean,
predicate: (item: T) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item, readIndex) === true) {
if (predicate(item) === true) {
array[writeIndex++] = item;
}
}

View File

@@ -1,230 +0,0 @@
/**
* 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.
*/
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
loc: value.loc,
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
if (errors.hasErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.push({
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
}
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}
```
## Error
```
Found 1 error:
Error: Expected a non-reserved identifier name
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -1,13 +0,0 @@
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}
```
## Error
```
Found 1 error:
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
<unknown> thunk$14.
error.bug-infer-mutation-aliasing-effects.ts:10:22
8 | function thunk(action) {
9 | if (typeof action === 'function') {
> 10 | return action(thunk, () => stateRef.current, extraArg);
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
11 | } else {
12 | dispatch(action);
13 | return undefined;
```

View File

@@ -1,18 +0,0 @@
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}

View File

@@ -1,31 +0,0 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -1,5 +0,0 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -1,37 +0,0 @@
## Input
```javascript
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}
```
## Error
```
Found 1 error:
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
5 | try {
6 | // do something
> 7 | } catch ({status}) {
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
8 | // do something
9 | }
10 | }, []);
```

View File

@@ -1,11 +0,0 @@
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}
```
## Error
```
Found 1 error:
Invariant: Expected a break target
```

View File

@@ -1,15 +0,0 @@
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}

View File

@@ -1,44 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};
```
## Error
```
Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |
8 | if (data) {
> 9 | ({a, b} = data);
| ^ Expected consistent kind for destructuring
10 | }
11 |
12 | const foo = useFoo(a);
```

View File

@@ -1,16 +0,0 @@
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};
```
## Error
```
Found 1 error:
Invariant: Expected all references to a variable to be consistently local or context references
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
error.bug-invariant-local-or-context-references.ts:15:13
13 | setState(_prevState => ({
14 | loading: false,
> 15 | error: err,
| ^^^ Expected all references to a variable to be consistently local or context references
16 | }));
17 | }
18 | };
```

View File

@@ -1,18 +0,0 @@
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};

View File

@@ -1,34 +0,0 @@
## Input
```javascript
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};
```
## Error
```
Found 1 error:
Invariant: Unexpected terminal in optional
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
1 | const Foo = ({json}) => {
2 | try {
> 3 | const foo = JSON.parse(json)?.foo;
| ^^^^ Unexpected terminal in optional
4 | return <span>{foo}</span>;
5 | } catch {
6 | return null;
```

View File

@@ -1,8 +0,0 @@
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};

View File

@@ -1,30 +0,0 @@
## Input
```javascript
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}
```
## Error
```
Found 1 error:
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
identifier 15 is unnamed.
```

View File

@@ -1,11 +0,0 @@
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}
```
## Error
```
Found 1 error:
Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
error.invalid-derived-computation-in-effect.ts:9:4
7 | const [fullName, setFullName] = useState('');
8 | useEffect(() => {
> 9 | setFullName(capitalize(firstName + ' ' + lastName));
| ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
10 | }, [firstName, lastName]);
11 |
12 | return <div>{fullName}</div>;
```

View File

@@ -1,13 +0,0 @@
// @validateNoDerivedComputationsInEffects
function BadExample() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(capitalize(firstName + ' ' + lastName));
}, [firstName, lastName]);
return <div>{fullName}</div>;
}

View File

@@ -879,7 +879,7 @@ function initializeDebugChunk(
waitForReference(
debugChunk,
{}, // noop, since we'll have already added an entry to debug info
'debug', // noop, but we need it to not be empty string since that indicates the root object
'', // noop
response,
initializeDebugInfo,
[''], // path

View File

@@ -490,7 +490,7 @@ export function logComponentAwait(
if (typeof value === 'object' && value !== null) {
addObjectToProperties(value, properties, 0, '');
} else if (value !== undefined) {
addValueToProperties('awaited value', value, properties, 0, '');
addValueToProperties('Resolved', value, properties, 0, '');
}
const tooltipText = getIOLongName(
asyncInfo.awaited,
@@ -547,7 +547,7 @@ export function logIOInfoErrored(
String(error.message)
: // eslint-disable-next-line react-internal/safe-string-coercion
String(error);
const properties = [['rejected with', message]];
const properties = [['Rejected', message]];
const tooltipText =
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
debugTask.run(

View File

@@ -52,7 +52,7 @@ test.describe('Components', () => {
test('Should allow elements to be inspected', async () => {
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>');
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Prop names/values may not be editable based on the React version.
// If they're not editable, make sure they degrade gracefully
@@ -119,7 +119,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>', true);
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true);
// Then read the inspected values.
const sourceText = await page.evaluate(() => {
@@ -142,7 +142,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the first list item in DevTools.
await devToolsUtils.selectElement(page, 'ListItem', '<List>\n<App>');
await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp');
// Then edit the label prop.
await page.evaluate(() => {
@@ -177,7 +177,7 @@ test.describe('Components', () => {
runOnlyForReactRange('>=16.8');
// Select the List component DevTools.
await devToolsUtils.selectElement(page, 'List', '<App>');
await devToolsUtils.selectElement(page, 'List', 'App');
// Then click to load and parse hook names.
await devToolsUtils.clickButton(page, 'LoadHookNamesButton');

View File

@@ -17,10 +17,8 @@ describe('Store', () => {
let act;
let actAsync;
let bridge;
let createDisplayNameFilter;
let getRendererID;
let legacyRender;
let previousComponentFilters;
let store;
let withErrorsOrWarningsIgnored;
@@ -31,8 +29,6 @@ describe('Store', () => {
bridge = global.bridge;
store = global.store;
previousComponentFilters = store.componentFilters;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
@@ -42,14 +38,9 @@ describe('Store', () => {
actAsync = utils.actAsync;
getRendererID = utils.getRendererID;
legacyRender = utils.legacyRender;
createDisplayNameFilter = utils.createDisplayNameFilter;
withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
});
afterEach(() => {
store.componentFilters = previousComponentFilters;
});
const {render, unmount, createContainer} = getVersionedRenderImplementation();
// @reactVersion >= 18.0
@@ -138,72 +129,6 @@ describe('Store', () => {
`);
});
it('should handle reorder of filtered elements', async () => {
function IgnoreMePassthrough({children}) {
return children;
}
function PassThrough({children}) {
return children;
}
await actAsync(
async () =>
(store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]),
);
await act(() => {
render(
<PassThrough key="e" name="e">
<IgnoreMePassthrough key="e1">
<PassThrough name="e-child-one">
<p>e1</p>
</PassThrough>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e2">
<PassThrough name="e-child-two">
<div>e2</div>
</PassThrough>
</IgnoreMePassthrough>
</PassThrough>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <PassThrough key="e">
▾ <PassThrough>
<p>
▾ <PassThrough>
<div>
`);
await act(() => {
render(
<PassThrough key="e" name="e">
<IgnoreMePassthrough key="e2">
<PassThrough name="e-child-two">
<div>e2</div>
</PassThrough>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e1">
<PassThrough name="e-child-one">
<p>e1</p>
</PassThrough>
</IgnoreMePassthrough>
</PassThrough>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <PassThrough key="e">
▾ <PassThrough>
<div>
▾ <PassThrough>
<p>
`);
});
describe('StrictMode compliance', () => {
it('should mark strict root elements as strict', async () => {
const App = () => <Component />;

View File

@@ -207,11 +207,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Activity>
<div>
<Activity>
`);
[root]
▾ <Activity>
<div>
<Activity>
<div>
`);
await actAsync(
async () =>
@@ -221,9 +222,10 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
<div>
`);
[root]
<div>
<div>
`);
await actAsync(
async () =>
@@ -233,11 +235,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Activity>
<div>
<Activity>
`);
[root]
▾ <Activity>
<div>
<Activity>
<div>
`);
}
});
@@ -259,12 +262,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
await actAsync(
async () =>
@@ -274,12 +277,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
await actAsync(
async () =>
@@ -289,12 +292,12 @@ describe('Store component filters', () => {
);
expect(store).toMatchInlineSnapshot(`
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
[root]
▾ <ViewTransition>
<div>
▾ <ViewTransition>
<div>
`);
}
});

View File

@@ -19,8 +19,8 @@ import {
formatWithStyles,
gt,
gte,
parseSourceFromComponentStack,
} from 'react-devtools-shared/src/backend/utils';
import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
REACT_STRICT_MODE_TYPE as StrictMode,
@@ -306,20 +306,20 @@ describe('utils', () => {
});
});
describe('extractLocationFromComponentStack', () => {
describe('parseSourceFromComponentStack', () => {
it('should return null if passed empty string', () => {
expect(extractLocationFromComponentStack('')).toEqual(null);
expect(parseSourceFromComponentStack('')).toEqual(null);
});
it('should construct the source from the first frame if available', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' +
'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
),
).toEqual([
'l',
'',
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
1,
10389,
@@ -328,7 +328,7 @@ describe('utils', () => {
it('should construct the source from highest available frame', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
' at Q\n' +
' at a\n' +
' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' +
@@ -342,7 +342,7 @@ describe('utils', () => {
' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
),
).toEqual([
'm',
'',
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
5,
9236,
@@ -351,7 +351,7 @@ describe('utils', () => {
it('should construct the source from frame, which has only url specified', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
' at Q\n' +
' at a\n' +
' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
@@ -366,13 +366,13 @@ describe('utils', () => {
it('should parse sourceURL correctly if it includes parentheses', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' +
' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
),
).toEqual([
'HotReload',
'',
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
307,
11,
@@ -381,13 +381,13 @@ describe('utils', () => {
it('should support Firefox stack', () => {
expect(
extractLocationFromComponentStack(
parseSourceFromComponentStack(
'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' +
'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
),
).toEqual([
'tt',
'',
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
1,
165558,
@@ -401,7 +401,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.f = f;
function f() { }
//# sourceMappingURL=`;
const result = ['', 'http://test/a.mts', 1, 17];
const result = ['', 'http://test/a.mts', 1, 16];
const fs = {
'http://test/a.mts': `export function f() {}`,
'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,12 @@ export function formatOwnerStack(error: Error): string {
const prevPrepareStackTrace = Error.prepareStackTrace;
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
let stack = error.stack;
const stack = error.stack;
Error.prepareStackTrace = prevPrepareStackTrace;
return formatOwnerStackString(stack);
}
export function formatOwnerStackString(stack: string): string {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.

View File

@@ -12,11 +12,14 @@ import {compareVersions} from 'compare-versions';
import {dehydrate} from 'react-devtools-shared/src/hydration';
import isArray from 'shared/isArray';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
export {default as formatWithStyles} from './formatWithStyles';
export {default as formatConsoleArguments} from './formatConsoleArguments';
import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';
// TODO: update this to the first React version that has a corresponding DevTools backend
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
@@ -255,6 +258,186 @@ export const isReactNativeEnvironment = (): boolean => {
return window.document == null;
};
function extractLocation(url: string): null | {
functionName?: string,
sourceURL: string,
line?: string,
column?: string,
} {
if (url.indexOf(':') === -1) {
return null;
}
// remove any parentheses from start and end
const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, '');
const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec(
withoutParentheses,
);
if (locationParts == null) {
return null;
}
const functionName = ''; // TODO: Parse this in the regexp.
const [, , sourceURL, line, column] = locationParts;
return {functionName, sourceURL, line, column};
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
function parseSourceFromChromeStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();
const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/);
const possibleLocation = locationInParenthesesMatch
? locationInParenthesesMatch[1]
: sanitizedFrame;
const location = extractLocation(possibleLocation);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}
const {functionName, sourceURL, line = '1', column = '1'} = location;
return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
}
function parseSourceFromFirefoxStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
const sanitizedFrame = frame.trim();
const frameWithoutFunctionName = sanitizedFrame.replace(
/((.*".+"[^@]*)?[^@]*)(?:@)/,
'',
);
const location = extractLocation(frameWithoutFunctionName);
// Continue the search until at least sourceURL is found
if (location == null) {
continue;
}
const {functionName, sourceURL, line = '1', column = '1'} = location;
return [
functionName || '',
sourceURL,
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
}
export function parseSourceFromComponentStack(
componentStack: string,
): ReactFunctionLocation | null {
if (componentStack.match(CHROME_STACK_REGEXP)) {
return parseSourceFromChromeStack(componentStack);
}
return parseSourceFromFirefoxStack(componentStack);
}
let collectedLocation: ReactFunctionLocation | null = null;
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
let result: null | ReactFunctionLocation = null;
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = 0; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
const name = callSite.getFunctionName();
if (
name != null &&
(name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame'))
) {
// We pick the last frame that matches before the bottom frame since
// that will be immediately inside the component as opposed to some helper.
// If we don't find a bottom frame then we bail to string parsing.
collectedLocation = result;
// Skip everything after the bottom frame since it'll be internals.
break;
} else {
const sourceURL = callSite.getScriptNameOrSourceURL();
const line =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber()
: callSite.getLineNumber();
const col =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber()
: callSite.getColumnNumber();
if (!sourceURL || !line || !col) {
// Skip eval etc. without source url. They don't have location.
continue;
}
result = [name, sourceURL, line, col];
}
}
// At the same time we generate a string stack trace just in case someone
// else reads it.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
export function parseSourceFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
// First attempt to collected the structured data using prepareStackTrace.
collectedLocation = null;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = error.stack;
} catch (e) {
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
stack = error.stack;
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedLocation !== null) {
return collectedLocation;
}
if (stack == null) {
return null;
}
// Fallback to parsing the string form.
const componentStack = formatOwnerStackString(stack);
return parseSourceFromComponentStack(componentStack);
}
// 0.123456789 => 0.123
// Expects high-resolution timestamp in milliseconds, like from performance.now()
// Mainly used for optimizing the size of serialized profiling payload

View File

@@ -1,331 +0,0 @@
/**
/**
* 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
*/
import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes';
function parseStackTraceFromChromeStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.
stack = stack.slice(29);
}
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
// Cut off everything after the bottom frame since it'll be internals.
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
// We skip top frames here since they may or may not be parseable but we
// want to skip the same number of frames regardless. I.e. we can't do it
// in the caller.
for (let i = skipFrames; i < frames.length; i++) {
const parsed = chromeFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
let name = parsed[1] || '';
let isAsync = parsed[8] === 'async ';
if (name === '<anonymous>') {
name = '';
} else if (name.startsWith('async ')) {
name = name.slice(5);
isAsync = true;
}
let filename = parsed[2] || parsed[5] || '';
if (filename === '<anonymous>') {
filename = '';
}
const line = +(parsed[3] || parsed[6]);
const col = +(parsed[4] || parsed[7]);
parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
}
return parsedFrames;
}
const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/;
function parseStackTraceFromFirefoxStack(
stack: string,
skipFrames: number,
): ReactStackTrace {
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}
if (idx !== -1) {
// Cut off everything after the bottom frame since it'll be internals.
stack = stack.slice(0, idx);
}
const frames = stack.split('\n');
const parsedFrames: ReactStackTrace = [];
// We skip top frames here since they may or may not be parseable but we
// want to skip the same number of frames regardless. I.e. we can't do it
// in the caller.
for (let i = skipFrames; i < frames.length; i++) {
const parsed = firefoxFrameRegExp.exec(frames[i]);
if (!parsed) {
continue;
}
const name = parsed[1] || '';
const filename = parsed[2] || '';
const line = +parsed[3];
const col = +parsed[4];
parsedFrames.push([name, filename, line, col, 0, 0, false]);
}
return parsedFrames;
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
export function parseStackTraceFromString(
stack: string,
skipFrames: number,
): ReactStackTrace {
if (stack.match(CHROME_STACK_REGEXP)) {
return parseStackTraceFromChromeStack(stack, skipFrames);
}
return parseStackTraceFromFirefoxStack(stack, skipFrames);
}
let framesToSkip: number = 0;
let collectedStackTrace: null | ReactStackTrace = null;
const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
function getMethodCallName(callSite: CallSite): string {
const typeName = callSite.getTypeName();
const methodName = callSite.getMethodName();
const functionName = callSite.getFunctionName();
let result = '';
if (functionName) {
if (
typeName &&
identifierRegExp.test(functionName) &&
functionName !== typeName
) {
result += typeName + '.';
}
result += functionName;
if (
methodName &&
functionName !== methodName &&
!functionName.endsWith('.' + methodName) &&
!functionName.endsWith(' ' + methodName)
) {
result += ' [as ' + methodName + ']';
}
} else {
if (typeName) {
result += typeName + '.';
}
if (methodName) {
result += methodName;
} else {
result += '<anonymous>';
}
}
return result;
}
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
const result: ReactStackTrace = [];
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
let name = callSite.getFunctionName() || '<anonymous>';
if (
name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame')
) {
// Skip everything after the bottom frame since it'll be internals.
break;
} else if (callSite.isNative()) {
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([name, '', 0, 0, 0, 0, isAsync]);
} else {
// We encode complex function calls as if they're part of the function
// name since we cannot simulate the complex ones and they look the same
// as function names in UIs on the client as well as stacks.
if (callSite.isConstructor()) {
name = 'new ' + name;
} else if (!callSite.isToplevel()) {
name = getMethodCallName(callSite);
}
if (name === '<anonymous>') {
name = '';
}
let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
if (filename === '<anonymous>') {
filename = '';
if (callSite.isEval()) {
const origin = callSite.getEvalOrigin();
if (origin) {
filename = origin.toString() + ', <anonymous>';
}
}
}
const line = callSite.getLineNumber() || 0;
const col = callSite.getColumnNumber() || 0;
const enclosingLine: number =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber() || 0
: 0;
const enclosingCol: number =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber() || 0
: 0;
// $FlowFixMe[prop-missing]
const isAsync = callSite.isAsync();
result.push([
name,
filename,
line,
col,
enclosingLine,
enclosingCol,
isAsync,
]);
}
}
collectedStackTrace = result;
// At the same time we generate a string stack trace just in case someone
// else reads it. Ideally, we'd call the previous prepareStackTrace to
// ensure it's in the expected format but it's common for that to be
// source mapped and since we do a lot of eager parsing of errors, it
// would be slow in those environments. We could maybe just rely on those
// environments having to disable source mapping globally to speed things up.
// For now, we just generate a default V8 formatted stack trace without
// source mapping as a fallback.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
// This matches either of these V8 formats.
// at name (filename:0:0)
// at filename:0:0
// at async filename:0:0
const chromeFrameRegExp =
/^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
export function parseStackTrace(
error: Error,
skipFrames: number,
): ReactStackTrace {
// We can only get structured data out of error objects once. So we cache the information
// so we can get it again each time. It also helps performance when the same error is
// referenced more than once.
const existing = stackTraceCache.get(error);
if (existing !== undefined) {
return existing;
}
// We override Error.prepareStackTrace with our own version that collects
// the structured data. We need more information than the raw stack gives us
// and we need to ensure that we don't get the source mapped version.
collectedStackTrace = null;
framesToSkip = skipFrames;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = String(error.stack);
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedStackTrace !== null) {
const result = collectedStackTrace;
collectedStackTrace = null;
stackTraceCache.set(error, result);
return result;
}
// If the stack has already been read, or this is not actually a V8 compatible
// engine then we might not get a normalized stack and it might still have been
// source mapped. Regardless we try our best to parse it.
const parsedFrames = parseStackTraceFromString(stack, skipFrames);
stackTraceCache.set(error, parsedFrames);
return parsedFrames;
}
export function extractLocationFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
const stackTrace = parseStackTrace(error, 0);
const stack = error.stack;
if (
!stack.includes('react_stack_bottom_frame') &&
!stack.includes('react-stack-bottom-frame')
) {
// This didn't have a bottom to it, we can't trust it.
return null;
}
// We start from the bottom since that will have the best location for the owner itself.
for (let i = stackTrace.length - 1; i >= 0; i--) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
// Take the first match with a colon in the file name.
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
// Use enclosing line if available, since that points to the start of the function.
encLine || line,
encCol || col,
];
}
}
return null;
}
export function extractLocationFromComponentStack(
stack: string,
): ReactFunctionLocation | null {
const stackTrace = parseStackTraceFromString(stack, 0);
for (let i = 0; i < stackTrace.length; i++) {
const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
// Take the first match with a colon in the file name.
if (fileName.indexOf(':') !== -1) {
return [
functionName,
fileName,
// Use enclosing line if available. (Never the case here because we parse from string.)
encLine || line,
encCol || col,
];
}
}
return null;
}

View File

@@ -34,12 +34,6 @@ export type IconType =
| 'save'
| 'search'
| 'settings'
| 'panel-left-close'
| 'panel-left-open'
| 'panel-right-close'
| 'panel-right-open'
| 'panel-bottom-open'
| 'panel-bottom-close'
| 'error'
| 'suspend'
| 'undo'
@@ -52,10 +46,8 @@ type Props = {
type: IconType,
};
const materialIconsViewBox = '0 -960 960 960';
export default function ButtonIcon({className = '', type}: Props): React.Node {
let pathData = null;
let viewBox = '0 0 24 24';
switch (type) {
case 'add':
pathData = PATH_ADD;
@@ -129,30 +121,6 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
case 'error':
pathData = PATH_ERROR;
break;
case 'panel-left-close':
pathData = PATH_MATERIAL_PANEL_LEFT_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'panel-left-open':
pathData = PATH_MATERIAL_PANEL_LEFT_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-right-close':
pathData = PATH_MATERIAL_PANEL_RIGHT_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'panel-right-open':
pathData = PATH_MATERIAL_PANEL_RIGHT_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-open':
pathData = PATH_MATERIAL_PANEL_BOTTOM_OPEN;
viewBox = materialIconsViewBox;
break;
case 'panel-bottom-close':
pathData = PATH_MATERIAL_PANEL_BOTTOM_CLOSE;
viewBox = materialIconsViewBox;
break;
case 'suspend':
pathData = PATH_SUSPEND;
break;
@@ -179,7 +147,7 @@ export default function ButtonIcon({className = '', type}: Props): React.Node {
className={`${styles.ButtonIcon} ${className}`}
width="24"
height="24"
viewBox={viewBox}>
viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
{typeof pathData === 'string' ? (
<path fill="currentColor" d={pathData} />
@@ -308,33 +276,3 @@ const PATH_VIEW_SOURCE = `
const PATH_EDITOR = `
M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z
`;
// Source: Material Design Icons left_panel_close
const PATH_MATERIAL_PANEL_LEFT_CLOSE = `
M648-324v-312L480-480l168 156ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
`;
// Source: Material Design Icons left_panel_open
const PATH_MATERIAL_PANEL_LEFT_OPEN = `
M504-595v230q0 12.25 10.5 16.62Q525-344 534-352l110-102q11-11.18 11-26.09T644-506L534-608q-8.82-8-19.41-3.5T504-595ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm125-72v-528H216v528h120Zm72 0h336v-528H408v528Zm-72 0H216h120Z
`;
// Source: Material Design Icons right_panel_close
const PATH_MATERIAL_PANEL_RIGHT_CLOSE = `
M312-365q0 12.25 10.5 16.62Q333-344 342-352l110-102q11-11.18 11-26.09T452-506L342-608q-8.82-8-19.41-3.5T312-595v230ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
`;
// Source: Material Design Icons right_panel_open
const PATH_MATERIAL_PANEL_RIGHT_OPEN = `
M456-365v-230q0-12.25-10.5-16.63Q435-616 426-608L316-506q-11 11.18-11 26.09T316-454l110 102q8.82 8 19.41 3.5T456-365ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm413-72h120v-528H624v528Zm-72 0v-528H216v528h336Zm72 0h120-120Z
`;
// Source: Material Design Icons bottom_panel_open
const PATH_MATERIAL_PANEL_BOTTOM_OPEN = `
M365-504h230q12.25 0 16.63-10.5Q616-525 608-534L506-644q-11.18-11-26.09-11T454-644L352-534q-8 8.82-3.5 19.41T365-504ZM211-144q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211Zm5-192v120h528v-120H216Zm0-72h528v-336H216v336Zm0 72v120-120Z
`;
// Source: Material Design Icons bottom_panel_close
const PATH_MATERIAL_PANEL_BOTTOM_CLOSE = `
m506-508 102-110q8-8.82 3.5-19.41T595-648H365q-12.25 0-16.62 10.5Q344-627 352-618l102 110q11.18 11 26.09 11T506-508Zm243-308q27.64 0 47.32 19.68T816-749v538q0 27.64-19.68 47.32T749-144H211q-27.64 0-47.32-19.68T144-211v-538q0-27.64 19.68-47.32T211-816h538ZM216-336v120h528v-120H216Zm528-72v-336H216v336h528Zm-528 72v120-120Z
`;

View File

@@ -97,11 +97,11 @@
}
.CollapsableContent {
margin-top: -0.25rem;
padding: 0.25rem 0;
}
.PreviewContainer {
padding: 0.25rem;
padding: 0 0.25rem 0.25rem 0.25rem;
}
.TimeBarContainer {
@@ -123,10 +123,3 @@
.TimeBarSpanErrored {
background-color: var(--color-timespan-background-errored);
}
.SmallHeader {
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
padding-left: 1.25rem;
margin-top: 0.25rem;
}

View File

@@ -80,13 +80,21 @@ function SuspendedByRow({
maxTime,
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const ioInfo = asyncInfo.awaited;
const name = ioInfo.name;
const description = ioInfo.description;
const name = asyncInfo.awaited.name;
const description = asyncInfo.awaited.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
const start = ioInfo.start;
const end = ioInfo.end;
let stack;
let owner;
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
stack = asyncInfo.awaited.stack;
owner = asyncInfo.awaited.owner;
} else {
stack = asyncInfo.stack;
owner = asyncInfo.owner;
}
const start = asyncInfo.awaited.start;
const end = asyncInfo.awaited.end;
const timeScale = 100 / (maxTime - minTime);
let left = (start - minTime) * timeScale;
let width = (end - start) * timeScale;
@@ -98,23 +106,12 @@ function SuspendedByRow({
}
}
const ioOwner = ioInfo.owner;
const asyncOwner = asyncInfo.owner;
const showIOStack = ioInfo.stack !== null && ioInfo.stack.length !== 0;
// Only show the awaited stack if the I/O started in a different owner
// than where it was awaited. If it's started by the same component it's
// probably easy enough to infer and less noise in the common case.
const showAwaitStack =
!showIOStack ||
(ioOwner === null
? asyncOwner !== null
: asyncOwner === null || ioOwner.id !== asyncOwner.id);
const value: any = asyncInfo.awaited.value;
const isErrored =
value !== null &&
typeof value === 'object' &&
value[meta.name] === 'rejected Thenable';
const value: any = ioInfo.value;
const metaName =
value !== null && typeof value === 'object' ? value[meta.name] : null;
const isFulfilled = metaName === 'fulfilled Thenable';
const isRejected = metaName === 'rejected Thenable';
return (
<div className={styles.CollapsableRow}>
<Button
@@ -139,7 +136,7 @@ function SuspendedByRow({
<div className={styles.TimeBarContainer}>
<div
className={
!isRejected ? styles.TimeBarSpan : styles.TimeBarSpanErrored
!isErrored ? styles.TimeBarSpan : styles.TimeBarSpanErrored
}
style={{
left: left.toFixed(2) + '%',
@@ -150,39 +147,6 @@ function SuspendedByRow({
</Button>
{isOpen && (
<div className={styles.CollapsableContent}>
{showIOStack && <StackTraceView stack={ioInfo.stack} />}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
<OwnerView
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
hocDisplayNames={ioOwner.hocDisplayNames}
compiledWithForget={ioOwner.compiledWithForget}
id={ioOwner.id}
isInStore={store.containsElement(ioOwner.id)}
type={ioOwner.type}
/>
) : null}
{showAwaitStack ? (
<>
<div className={styles.SmallHeader}>awaited at:</div>
{asyncInfo.stack !== null && asyncInfo.stack.length > 0 && (
<StackTraceView stack={asyncInfo.stack} />
)}
{asyncOwner !== null && asyncOwner.id !== inspectedElement.id ? (
<OwnerView
key={asyncOwner.id}
displayName={asyncOwner.displayName || 'Anonymous'}
hocDisplayNames={asyncOwner.hocDisplayNames}
compiledWithForget={asyncOwner.compiledWithForget}
id={asyncOwner.id}
isInStore={store.containsElement(asyncOwner.id)}
type={asyncOwner.type}
/>
) : null}
</>
) : null}
<div className={styles.PreviewContainer}>
<KeyValue
alphaSort={true}
@@ -194,27 +158,27 @@ function SuspendedByRow({
element={element}
hidden={false}
inspectedElement={inspectedElement}
name={
isFulfilled
? 'awaited value'
: isRejected
? 'rejected with'
: 'pending value'
}
path={
isFulfilled
? [index, 'awaited', 'value', 'value']
: isRejected
? [index, 'awaited', 'value', 'reason']
: [index, 'awaited', 'value']
}
name={'Promise'}
path={[index, 'awaited', 'value']}
pathRoot="suspendedBy"
store={store}
value={
isFulfilled ? value.value : isRejected ? value.reason : value
}
value={asyncInfo.awaited.value}
/>
</div>
{stack !== null && stack.length > 0 && (
<StackTraceView stack={stack} />
)}
{owner !== null && owner.id !== inspectedElement.id ? (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
) : null}
</div>
)}
</div>
@@ -228,15 +192,6 @@ type Props = {
store: Store,
};
function compareTime(a: SerializedAsyncInfo, b: SerializedAsyncInfo): number {
const ioA = a.awaited;
const ioB = b.awaited;
if (ioA.start === ioB.start) {
return ioA.end - ioB.end;
}
return ioA.start - ioB.start;
}
export default function InspectedElementSuspendedBy({
bridge,
element,
@@ -273,9 +228,6 @@ export default function InspectedElementSuspendedBy({
minTime = maxTime - 25;
}
const sortedSuspendedBy = suspendedBy.slice(0);
sortedSuspendedBy.sort(compareTime);
return (
<div>
<div className={styles.HeaderRow}>
@@ -284,7 +236,7 @@ export default function InspectedElementSuspendedBy({
<ButtonIcon type="copy" />
</Button>
</div>
{sortedSuspendedBy.map((asyncInfo, index) => (
{suspendedBy.map((asyncInfo, index) => (
<SuspendedByRow
key={index}
index={index}

View File

@@ -210,13 +210,6 @@ export default function KeyValue({
canRenameTheCurrentPath = canRenamePathsAtDepth(depth);
}
const hasChildren =
typeof value === 'object' &&
value !== null &&
(canEditValues ||
(isArray(value) && value.length > 0) ||
Object.entries(value).length > 0);
let renderedName;
if (isDirectChildOfAnArray) {
if (canDeletePaths) {
@@ -225,37 +218,27 @@ export default function KeyValue({
);
} else {
renderedName = (
<span
className={styles.Name}
onClick={isInspectable || hasChildren ? toggleIsOpen : null}>
<span className={styles.Name}>
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
<span className={styles.AfterName}>:</span>
</span>
);
}
} else if (canRenameTheCurrentPath) {
renderedName = (
<>
<EditableName
allowEmpty={canDeletePaths}
className={styles.EditableName}
initialValue={name}
overrideName={renamePath}
path={path}
/>
<span className={styles.AfterName}>:</span>
</>
<EditableName
allowEmpty={canDeletePaths}
className={styles.EditableName}
initialValue={name}
overrideName={renamePath}
path={path}
/>
);
} else {
renderedName = (
<span
className={styles.Name}
data-testname="NonEditableName"
onClick={isInspectable || hasChildren ? toggleIsOpen : null}>
<span className={styles.Name} data-testname="NonEditableName">
{name}
{!!hookName && <span className={styles.HookName}>({hookName})</span>}
<span className={styles.AfterName}>:</span>
</span>
);
}
@@ -303,6 +286,7 @@ export default function KeyValue({
style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
{renderedName}
<div className={styles.AfterName}>:</div>
{canEditValues ? (
<EditableValue
overrideValue={overrideValue}
@@ -344,6 +328,7 @@ export default function KeyValue({
style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Link}
onClick={() => {
@@ -380,6 +365,7 @@ export default function KeyValue({
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={isInspectable ? toggleIsOpen : undefined}>
@@ -402,6 +388,7 @@ export default function KeyValue({
}
} else {
if (isArray(value)) {
const hasChildren = value.length > 0 || canEditValues;
const displayName = getMetaValueLabel(value);
children = value.map((innerValue, index) => (
@@ -462,11 +449,12 @@ export default function KeyValue({
ref={contextMenuTriggerRef}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -484,6 +472,7 @@ export default function KeyValue({
entries.sort(alphaSortEntries);
}
const hasChildren = entries.length > 0 || canEditValues;
const displayName = getMetaValueLabel(value);
children = entries.map(([key, keyValue]): ReactElement<any> => (
@@ -542,11 +531,12 @@ export default function KeyValue({
ref={contextMenuTriggerRef}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} />
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
{renderedName}
<div className={styles.AfterName}>:</div>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -577,10 +567,7 @@ function DeleteToggle({deletePath, name, path}) {
title="Delete entry">
<ButtonIcon type="delete" />
</Button>
<span className={styles.Name}>
{name}
<span className={styles.AfterName}>:</span>
</span>
<span className={styles.Name}>{name}</span>
</>
);
}

View File

@@ -2,7 +2,6 @@
color: var(--color-component-name);
font-family: var(--font-family-monospace);
font-size: var(--font-size-monospace-normal);
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -59,7 +59,7 @@ export default function OwnerView({
<span
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
title={displayName}>
{'<' + displayName + '>'}
{displayName}
</span>
<ElementBadges

View File

@@ -8,21 +8,12 @@
*/
import * as React from 'react';
import {use, useContext} from 'react';
import useOpenResource from '../useOpenResource';
import styles from './StackTraceView.css';
import type {
ReactStackTrace,
ReactCallSite,
ReactFunctionLocation,
} from 'shared/ReactTypes';
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
import formatLocationForDisplay from './formatLocationForDisplay';
@@ -31,23 +22,7 @@ type CallSiteViewProps = {
};
export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] =
callSite;
const symbolicatedCallSite: null | ReactFunctionLocation =
fetchFileWithCaching !== null
? use(
symbolicateSourceWithCache(
fetchFileWithCaching,
virtualURL,
virtualLine,
virtualColumn,
),
)
: null;
const symbolicatedCallSite: null | ReactCallSite = null; // TODO
const [linkIsEnabled, viewSource] = useOpenResource(
callSite,
symbolicatedCallSite,
@@ -56,7 +31,7 @@ export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
return (
<div className={styles.CallSite}>
{functionName || virtualFunctionName}
{functionName}
{' @ '}
<span
className={linkIsEnabled ? styles.Link : null}

View File

@@ -24,7 +24,7 @@ import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import Icon from '../Icon';
import {SettingsContext} from '../Settings/SettingsContext';
import {BridgeContext, StoreContext, OptionsContext} from '../context';
import ComponentsTreeElement from './Element';
import Element from './Element';
import InspectHostNodesToggle from './InspectHostNodesToggle';
import OwnersStack from './OwnersStack';
import ComponentSearchInput from './ComponentSearchInput';
@@ -93,47 +93,8 @@ export default function Tree(): React.Node {
const treeRef = useRef<HTMLDivElement | null>(null);
const focusTargetRef = useRef<HTMLDivElement | null>(null);
const listDOMElementRef = useRef<Element | null>(null);
const setListDOMElementRef = useCallback((listDOMElement: Element) => {
listDOMElementRef.current = listDOMElement;
// Controls the initial horizontal offset of the Tree if the element was pre-selected. For example, via Elements panel in browser DevTools.
// Initial vertical offset is controlled via initialScrollOffset prop of the FixedSizeList component.
if (
!componentsPanelVisible ||
inspectedElementIndex == null ||
listDOMElement == null
) {
return;
}
const element = store.getElementAtIndex(inspectedElementIndex);
if (element == null) {
return;
}
const viewportLeft = listDOMElement.scrollLeft;
const viewportRight = viewportLeft + listDOMElement.clientWidth;
const elementLeft = calculateElementOffset(element.depth);
// Because of virtualization, this element might not be rendered yet; we can't look up its width.
// Assuming that it may take up to the half of the viewport.
const elementRight = elementLeft + listDOMElement.clientWidth / 2;
const isElementFullyVisible =
elementLeft >= viewportLeft && elementRight <= viewportRight;
if (!isElementFullyVisible) {
const horizontalDelta =
Math.min(0, elementLeft - viewportLeft) +
Math.max(0, elementRight - viewportRight);
// $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior.
listDOMElement.scrollBy({
left: horizontalDelta,
behavior: 'instant',
});
}
}, []);
const listRef = useRef(null);
const listDOMElementRef = useRef(null);
useEffect(() => {
if (!componentsPanelVisible || inspectedElementIndex == null) {
@@ -157,7 +118,7 @@ export default function Tree(): React.Node {
}
const elementLeft = calculateElementOffset(element.depth);
// Because of virtualization, this element might not be rendered yet; we can't look up its width.
// Assuming that it may take up to the half of the viewport.
// Assuming that it may take up to the half of the vieport.
const elementRight = elementLeft + listDOMElement.clientWidth / 2;
const elementTop = inspectedElementIndex * lineHeight;
const elementBottom = elementTop + lineHeight;
@@ -176,7 +137,6 @@ export default function Tree(): React.Node {
Math.min(0, elementLeft - viewportLeft) +
Math.max(0, elementRight - viewportRight);
// $FlowExpectedError[incompatible-call] Flow doesn't support instant as an option for behavior.
listDOMElement.scrollBy({
top: verticalDelta,
left: horizontalDelta,
@@ -511,10 +471,11 @@ export default function Tree(): React.Node {
itemData={itemData}
itemKey={itemKey}
itemSize={lineHeight}
outerRef={setListDOMElementRef}
ref={listRef}
outerRef={listDOMElementRef}
overscanCount={10}
width={width}>
{ComponentsTreeElement}
{Element}
</FixedSizeList>
)}
</AutoSizer>

View File

@@ -803,45 +803,6 @@ type Props = {
defaultInspectedElementIndex?: ?number,
};
function getInitialState({
defaultOwnerID,
defaultInspectedElementID,
defaultInspectedElementIndex,
store,
}: {
defaultOwnerID?: ?number,
defaultInspectedElementID?: ?number,
defaultInspectedElementIndex?: ?number,
store: Store,
}): State {
return {
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
// Search
searchIndex: null,
searchResults: [],
searchText: '',
// Owners
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
ownerFlatTree: null,
// Inspection element panel
inspectedElementID:
defaultInspectedElementID != null
? defaultInspectedElementID
: store.lastSelectedHostInstanceElementId,
inspectedElementIndex:
defaultInspectedElementIndex != null
? defaultInspectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
};
}
// TODO Remove TreeContextController wrapper element once global Context.write API exists.
function TreeContextController({
children,
@@ -905,16 +866,32 @@ function TreeContextController({
[store],
);
const [state, dispatch] = useReducer(
reducer,
{
defaultOwnerID,
defaultInspectedElementID,
defaultInspectedElementIndex,
store,
},
getInitialState,
);
const [state, dispatch] = useReducer(reducer, {
// Tree
numElements: store.numElements,
ownerSubtreeLeafElementID: null,
// Search
searchIndex: null,
searchResults: [],
searchText: '',
// Owners
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
ownerFlatTree: null,
// Inspection element panel
inspectedElementID:
defaultInspectedElementID != null
? defaultInspectedElementID
: store.lastSelectedHostInstanceElementId,
inspectedElementIndex:
defaultInspectedElementIndex != null
? defaultInspectedElementIndex
: store.lastSelectedHostInstanceElementId
? store.getIndexOfElementID(store.lastSelectedHostInstanceElementId)
: null,
});
const transitionDispatch = useMemo(
() => (action: Action) =>
startTransition(() => {

View File

@@ -1,117 +0,0 @@
.SuspenseTab {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family-sans);
}
.SuspenseTab, .SuspenseTab * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}
.TreeWrapper {
flex: 1 1 var(--horizontal-resize-tree-percentage);
display: flex;
flex-direction: row;
overflow: auto;
border-top: 1px solid var(--color-border);
}
.InspectedElementWrapper {
flex: 1 1 calc(100% - var(--horizontal-resize-tree-percentage));
overflow-x: hidden;
overflow-y: auto;
}
.ResizeBarWrapper {
flex: 0 0 0px;
position: relative;
}
.ResizeBar {
position: absolute;
/*
* moving the bar out of its bounding box might cause its hitbox to overlap
* with another scrollbar creating disorienting UX where you both resize and scroll
* at the same time.
* If you adjust this value, double check that starting resize right on this edge
* doesn't also cause scroll
*/
left: 1px;
width: 5px;
height: 100%;
cursor: ew-resize;
}
.TreeView footer {
display: none;
}
@container devtools (width < 600px) {
.SuspenseTab {
flex-direction: column;
}
.TreeWrapper {
border-top: 1px solid var(--color-border);
flex: 1 0 var(--vertical-resize-tree-percentage);
}
.InspectedElementWrapper {
flex: 1 1 50%;
}
.TreeWrapper + .ResizeBarWrapper .ResizeBar {
top: 1px;
left: 0;
width: 100%;
height: 5px;
cursor: ns-resize;
}
.TreeView footer {
display: flex;
justify-content: end;
}
.ToggleInspectedElement[data-orientation="horizontal"] {
display: none;
}
}
.TreeList {
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
border-right: 1px solid var(--color-border);
padding: 0.25rem
}
.TreeView {
flex: 1 1 35%;
display: flex;
flex-direction: column;
}
.Rects {
border-top: 1px solid var(--color-border);
padding: 0.25rem;
flex-grow: 1;
}
.TimelineWrapper {
padding: 0.25rem;
display: flex;
flex-direction: row;
}
.Timeline {
flex-grow: 1;
}

View File

@@ -1,445 +1,8 @@
/**
* 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
*/
import * as React from 'react';
import {useEffect, useLayoutEffect, useReducer, useRef} from 'react';
import {
localStorageGetItem,
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import ButtonIcon, {type IconType} from '../ButtonIcon';
import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBoundary';
import InspectedElement from '../Components/InspectedElement';
import portaledContent from '../portaledContent';
import styles from './SuspenseTab.css';
import Button from '../Button';
type Orientation = 'horizontal' | 'vertical';
type LayoutActionType =
| 'ACTION_SET_TREE_LIST_TOGGLE'
| 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION'
| 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE'
| 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
| 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
type LayoutAction = {
type: LayoutActionType,
payload: any,
};
type LayoutState = {
treeListHidden: boolean,
treeListHorizontalFraction: number,
inspectedElementHidden: boolean,
inspectedElementHorizontalFraction: number,
inspectedElementVerticalFraction: number,
};
type LayoutDispatch = (action: LayoutAction) => void;
function SuspenseTreeList() {
return <div>tree list</div>;
function SuspenseTab() {
return 'Under construction';
}
function SuspenseTimeline() {
return <div className={styles.Timeline}>timeline</div>;
}
function SuspenseRects() {
return <div>rects</div>;
}
function ToggleTreeList({
dispatch,
state,
}: {
dispatch: LayoutDispatch,
state: LayoutState,
}) {
return (
<Button
onClick={() =>
dispatch({
type: 'ACTION_SET_TREE_LIST_TOGGLE',
payload: null,
})
}
title={state.treeListHidden ? 'Show Tree List' : 'Hide Tree List'}>
<ButtonIcon
type={state.treeListHidden ? 'panel-left-open' : 'panel-left-close'}
/>
</Button>
);
}
function ToggleInspectedElement({
dispatch,
state,
orientation,
}: {
dispatch: LayoutDispatch,
state: LayoutState,
orientation: 'horizontal' | 'vertical',
}) {
let iconType: IconType;
if (orientation === 'horizontal') {
iconType = state.inspectedElementHidden
? 'panel-right-open'
: 'panel-right-close';
} else {
iconType = state.inspectedElementHidden
? 'panel-bottom-open'
: 'panel-bottom-close';
}
return (
<Button
className={styles.ToggleInspectedElement}
data-orientation={orientation}
onClick={() =>
dispatch({
type: 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE',
payload: null,
})
}
title={
state.inspectedElementHidden
? 'Show Inspected Element'
: 'Hide Inspected Element'
}>
<ButtonIcon type={iconType} />
</Button>
);
}
function SuspenseTab(_: {}) {
const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
layoutReducer,
null,
initLayoutState,
);
const wrapperTreeRef = useRef<null | HTMLElement>(null);
const resizeTreeRef = useRef<null | HTMLElement>(null);
const resizeTreeListRef = useRef<null | HTMLElement>(null);
// TODO: We'll show the recently inspected element in this tab when it should probably
// switch to the nearest Suspense boundary when we switch into this tab.
const {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
treeListHidden,
treeListHorizontalFraction,
} = state;
useLayoutEffect(() => {
const wrapperElement = wrapperTreeRef.current;
setResizeCSSVariable(
wrapperElement,
'tree',
'horizontal',
inspectedElementHorizontalFraction * 100,
);
setResizeCSSVariable(
wrapperElement,
'tree',
'vertical',
inspectedElementVerticalFraction * 100,
);
const resizeTreeListElement = resizeTreeListRef.current;
setResizeCSSVariable(
resizeTreeListElement,
'tree-list',
'horizontal',
treeListHorizontalFraction * 100,
);
}, []);
useEffect(() => {
const timeoutID = setTimeout(() => {
localStorageSetItem(
LOCAL_STORAGE_KEY,
JSON.stringify({
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
treeListHidden,
treeListHorizontalFraction,
}),
);
}, 500);
return () => clearTimeout(timeoutID);
}, [
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
treeListHidden,
treeListHorizontalFraction,
]);
const onResizeStart = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
element.setPointerCapture(event.pointerId);
};
const onResizeEnd = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
element.releasePointerCapture(event.pointerId);
};
const onResizeTree = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
const isResizing = element.hasPointerCapture(event.pointerId);
if (!isResizing) {
return;
}
const resizeElement = resizeTreeRef.current;
const wrapperElement = wrapperTreeRef.current;
if (wrapperElement === null || resizeElement === null) {
return;
}
event.preventDefault();
const orientation = getTreeOrientation(wrapperElement);
const {height, width, left, top} = wrapperElement.getBoundingClientRect();
const currentMousePosition =
orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
const boundaryMin = MINIMUM_TREE_SIZE;
const boundaryMax =
orientation === 'horizontal'
? width - MINIMUM_TREE_SIZE
: height - MINIMUM_TREE_SIZE;
const isMousePositionInBounds =
currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
if (isMousePositionInBounds) {
const resizedElementDimension =
orientation === 'horizontal' ? width : height;
const actionType =
orientation === 'horizontal'
? 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION'
: 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION';
const fraction = currentMousePosition / resizedElementDimension;
const percentage = fraction * 100;
setResizeCSSVariable(wrapperElement, 'tree', orientation, percentage);
dispatch({
type: actionType,
payload: fraction,
});
}
};
const onResizeTreeList = (event: SyntheticPointerEvent<HTMLElement>) => {
const element = event.currentTarget;
const isResizing = element.hasPointerCapture(event.pointerId);
if (!isResizing) {
return;
}
const resizeElement = resizeTreeListRef.current;
const wrapperElement = resizeTreeRef.current;
if (wrapperElement === null || resizeElement === null) {
return;
}
event.preventDefault();
const orientation = 'horizontal';
const {height, width, left, top} = wrapperElement.getBoundingClientRect();
const currentMousePosition =
orientation === 'horizontal' ? event.clientX - left : event.clientY - top;
const boundaryMin = MINIMUM_TREE_LIST_SIZE;
const boundaryMax =
orientation === 'horizontal'
? width - MINIMUM_TREE_LIST_SIZE
: height - MINIMUM_TREE_LIST_SIZE;
const isMousePositionInBounds =
currentMousePosition > boundaryMin && currentMousePosition < boundaryMax;
if (isMousePositionInBounds) {
const resizedElementDimension =
orientation === 'horizontal' ? width : height;
const actionType = 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION';
const percentage = (currentMousePosition / resizedElementDimension) * 100;
setResizeCSSVariable(resizeElement, 'tree-list', orientation, percentage);
dispatch({
type: actionType,
payload: currentMousePosition / resizedElementDimension,
});
}
};
return (
<div className={styles.SuspenseTab} ref={wrapperTreeRef}>
<div className={styles.TreeWrapper} ref={resizeTreeRef}>
<div
className={styles.TreeList}
hidden={treeListHidden}
ref={resizeTreeListRef}>
<SuspenseTreeList />
</div>
<div className={styles.ResizeBarWrapper}>
<div
onPointerDown={onResizeStart}
onPointerMove={onResizeTreeList}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
</div>
<div className={styles.TreeView}>
<div className={styles.TimelineWrapper}>
<ToggleTreeList dispatch={dispatch} state={state} />
<SuspenseTimeline />
<ToggleInspectedElement
dispatch={dispatch}
state={state}
orientation="horizontal"
/>
</div>
<div className={styles.Rects}>
<SuspenseRects />
</div>
<footer>
<ToggleInspectedElement
dispatch={dispatch}
state={state}
orientation="vertical"
/>
</footer>
</div>
</div>
<div className={styles.ResizeBarWrapper}>
<div
onPointerDown={onResizeStart}
onPointerMove={onResizeTree}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
</div>
<div
className={styles.InspectedElementWrapper}
hidden={inspectedElementHidden}>
<InspectedElementErrorBoundary>
<InspectedElement />
</InspectedElementErrorBoundary>
</div>
</div>
);
}
const LOCAL_STORAGE_KEY = 'React::DevTools::SuspenseTab::layout';
const VERTICAL_TREE_MODE_MAX_WIDTH = 600;
const MINIMUM_TREE_SIZE = 100;
const MINIMUM_TREE_LIST_SIZE = 100;
function layoutReducer(state: LayoutState, action: LayoutAction): LayoutState {
switch (action.type) {
case 'ACTION_SET_TREE_LIST_TOGGLE':
return {
...state,
treeListHidden: !state.treeListHidden,
};
case 'ACTION_SET_TREE_LIST_HORIZONTAL_FRACTION':
return {
...state,
treeListHorizontalFraction: action.payload,
};
case 'ACTION_SET_INSPECTED_ELEMENT_TOGGLE':
return {
...state,
inspectedElementHidden: !state.inspectedElementHidden,
};
case 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION':
return {
...state,
inspectedElementHorizontalFraction: action.payload,
};
case 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION':
return {
...state,
inspectedElementVerticalFraction: action.payload,
};
default:
return state;
}
}
function initLayoutState(): LayoutState {
let inspectedElementHidden = false;
let inspectedElementHorizontalFraction = 0.65;
let inspectedElementVerticalFraction = 0.5;
let treeListHidden = false;
let treeListHorizontalFraction = 0.35;
try {
let data = localStorageGetItem(LOCAL_STORAGE_KEY);
if (data != null) {
data = JSON.parse(data);
inspectedElementHidden = data.inspectedElementHidden;
inspectedElementHorizontalFraction =
data.inspectedElementHorizontalFraction;
inspectedElementVerticalFraction = data.inspectedElementVerticalFraction;
treeListHidden = data.treeListHidden;
treeListHorizontalFraction = data.treeListHorizontalFraction;
}
} catch (error) {}
return {
inspectedElementHidden,
inspectedElementHorizontalFraction,
inspectedElementVerticalFraction,
treeListHidden,
treeListHorizontalFraction,
};
}
function getTreeOrientation(
wrapperElement: null | HTMLElement,
): null | Orientation {
if (wrapperElement != null) {
const {width} = wrapperElement.getBoundingClientRect();
return width > VERTICAL_TREE_MODE_MAX_WIDTH ? 'horizontal' : 'vertical';
}
return null;
}
function setResizeCSSVariable(
resizeElement: null | HTMLElement,
name: 'tree' | 'tree-list',
orientation: null | Orientation,
percentage: number,
): void {
if (resizeElement !== null && orientation !== null) {
resizeElement.style.setProperty(
`--${orientation}-resize-${name}-percentage`,
`${percentage}%`,
);
}
}
export default (portaledContent(SuspenseTab): React$ComponentType<{}>);
export default (portaledContent(SuspenseTab): React.ComponentType<{}>);

View File

@@ -21,8 +21,6 @@ import type {
InspectedElementPath,
} from 'react-devtools-shared/src/frontend/types';
import noop from 'shared/noop';
export const meta = {
inspectable: (Symbol('inspectable'): symbol),
inspected: (Symbol('inspected'): symbol),
@@ -319,15 +317,6 @@ export function dehydrate(
};
}
if (
data.status === 'resolved_model' ||
data.status === 'resolve_module'
) {
// This looks it's a lazy initialization pattern such in Flight.
// Since we're about to inspect it. Let's eagerly initialize it.
data.then(noop);
}
switch (data.status) {
case 'fulfilled': {
const unserializableValue: Unserializable = {

View File

@@ -17,7 +17,7 @@ const symbolicationCache: Map<
Promise<ReactFunctionLocation | null>,
> = new Map();
export function symbolicateSourceWithCache(
export async function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
@@ -82,14 +82,12 @@ export async function symbolicateSource(
const {
sourceURL: possiblyURL,
line,
column: columnZeroBased,
column,
} = consumer.originalPositionFor({
lineNumber, // 1-based
columnNumber, // 1-based
});
const column = columnZeroBased + 1;
if (possiblyURL === null) {
return null;
}

View File

@@ -10695,93 +10695,4 @@ describe('ReactDOMFizzServer', () => {
expect(getVisibleChildren(container)).toEqual(<div>Success!</div>);
});
it('should always flush the boundaries contributing the preamble regardless of their size', async () => {
const longDescription =
`I need to make this segment somewhat large because it needs to be large enought to be outlined during the initial flush. Setting the progressive chunk size to near zero isn't enough because there is a fixed minimum size that we use to avoid doing the size tracking altogether and this needs to be larger than that at least.
Unfortunately that previous paragraph wasn't quite long enough so I'll continue with some more prose and maybe throw on some repeated additional strings at the end for good measure.
` + 'a'.repeat(500);
const randomTag = Math.random().toString(36).slice(2, 10);
function App() {
return (
<Suspense fallback={randomTag}>
<html lang="en">
<body>
<main>{longDescription}</main>
</body>
</html>
</Suspense>
);
}
let streamedContent = '';
writable.on('data', chunk => (streamedContent += chunk));
await act(() => {
renderToPipeableStream(<App />, {progressiveChunkSize: 100}).pipe(
writable,
);
});
// We don't use the DOM here b/c we execute scripts which hides whether a fallback was shown briefly
// Instead we assert that we never emitted the fallback of the Suspense boundary around the body.
expect(streamedContent).not.toContain(randomTag);
});
it('should track byte size of shells that may contribute to the preamble when determining if the blocking render exceeds the max size', async () => {
const longDescription =
`I need to make this segment somewhat large because it needs to be large enought to be outlined during the initial flush. Setting the progressive chunk size to near zero isn't enough because there is a fixed minimum size that we use to avoid doing the size tracking altogether and this needs to be larger than that at least.
Unfortunately that previous paragraph wasn't quite long enough so I'll continue with some more prose and maybe throw on some repeated additional strings at the end for good measure.
` + 'a'.repeat(500);
const randomTag = Math.random().toString(36).slice(2, 10);
function App() {
return (
<>
<Suspense fallback={randomTag}>
<html lang="en">
<body>
<main>{longDescription}</main>
</body>
</html>
</Suspense>
<div>Outside Preamble</div>
</>
);
}
let streamedContent = '';
writable.on('data', chunk => (streamedContent += chunk));
const errors = [];
await act(() => {
renderToPipeableStream(<App />, {
progressiveChunkSize: 5,
onError(e) {
errors.push(e);
},
}).pipe(writable);
});
if (gate(flags => flags.enableFizzBlockingRender)) {
expect(errors.length).toBe(1);
expect(errors[0].message).toContain(
// We set the chunk size low enough that the threshold rounds to zero kB
'This rendered a large document (>0 kB) without any Suspense boundaries around most of it.',
);
} else {
expect(errors.length).toBe(0);
}
// We don't use the DOM here b/c we execute scripts which hides whether a fallback was shown briefly
// Instead we assert that we never emitted the fallback of the Suspense boundary around the body.
expect(streamedContent).not.toContain(randomTag);
});
});

View File

@@ -1915,57 +1915,4 @@ describe('ReactFlightDOMEdge', () => {
expect(ownerStack).toBeNull();
}
});
it('can pass an async import that resolves later as a prop to a null component', async () => {
let resolveClientComponentChunk;
const client = clientExports(
{
foo: 'bar',
},
'42',
'/test.js',
new Promise(resolve => (resolveClientComponentChunk = resolve)),
);
function ServerComponent(props) {
return null;
}
function App() {
return (
<div>
<ServerComponent client={client} />
</div>
);
}
const stream = await serverAct(() =>
passThrough(
ReactServerDOMServer.renderToReadableStream(<App />, webpackMap),
),
);
// Parsing the root blocks because the module hasn't loaded yet
const response = ReactServerDOMClient.createFromReadableStream(stream, {
serverConsumerManifest: {
moduleMap: null,
moduleLoading: null,
},
});
function ClientRoot() {
return use(response);
}
// Initialize to be blocked.
response.then(() => {});
// Unblock.
resolveClientComponentChunk();
const ssrStream = await serverAct(() =>
ReactDOMServer.renderToReadableStream(<ClientRoot />),
);
const result = await readResult(ssrStream);
expect(result).toEqual('<div></div>');
});
});

View File

@@ -438,50 +438,6 @@ describe('ReactFlightDOMReply', () => {
expect(response.obj).toBe(obj);
});
it('can return an opaque object through an async function', async () => {
function fn() {
return 'this is a client function';
}
const args = [fn];
const temporaryReferences =
ReactServerDOMClient.createTemporaryReferenceSet();
const body = await ReactServerDOMClient.encodeReply(args, {
temporaryReferences,
});
const temporaryReferencesServer =
ReactServerDOMServer.createTemporaryReferenceSet();
const serverPayload = await ReactServerDOMServer.decodeReply(
body,
webpackServerMap,
{temporaryReferences: temporaryReferencesServer},
);
async function action(arg) {
return arg;
}
const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
{
result: action.apply(null, serverPayload),
},
null,
{temporaryReferences: temporaryReferencesServer},
),
);
const response = await ReactServerDOMClient.createFromReadableStream(
stream,
{
temporaryReferences,
},
);
expect(await response.result).toBe(fn);
});
it('should supports streaming ReadableStream with objects', async () => {
let controller1;
let controller2;

View File

@@ -460,13 +460,7 @@ function isEligibleForOutlining(
// For very small boundaries, don't bother producing a fallback for outlining.
// The larger this limit is, the more we can save on preparing fallbacks in case we end up
// outlining.
return (
boundary.byteSize > 500 &&
// For boundaries that can possibly contribute to the preamble we don't want to outline
// them regardless of their size since the fallbacks should only be emitted if we've
// errored the boundary.
boundary.contentPreamble === null
);
return boundary.byteSize > 500;
}
function defaultErrorHandler(error: mixed) {
@@ -5504,9 +5498,6 @@ function preparePreambleFromSegment(
// This boundary is complete. It might have inner boundaries which are pending
// and able to provide a preamble so we have to check it's children
hoistPreambleState(request.renderState, preamble);
// We track this boundary's byteSize on the request since it will always flush with
// the request since it may contribute to the preamble
request.byteSize += boundary.byteSize;
const boundaryRootSegment = boundary.completedSegments[0];
if (!boundaryRootSegment) {
// Using the same error from flushSegment to avoid making a new one since conceptually the problem is still the same
@@ -5553,7 +5544,6 @@ function preparePreamble(request: Request) {
request.completedPreambleSegments === null
) {
const collectedPreambleSegments: Array<Array<Segment>> = [];
const originalRequestByteSize = request.byteSize;
const hasPendingPreambles = preparePreambleFromSegment(
request,
request.completedRootSegment,
@@ -5561,10 +5551,6 @@ function preparePreamble(request: Request) {
);
if (isPreambleReady(request.renderState, hasPendingPreambles)) {
request.completedPreambleSegments = collectedPreambleSegments;
} else {
// We restore the original size since the preamble is not ready
// and we will prepare it again.
request.byteSize = originalRequestByteSize;
}
}
}

View File

@@ -70,13 +70,6 @@ const proxyHandlers = {
`Instead, you can export a Client Component wrapper ` +
`that itself renders a Client Context Provider.`,
);
case 'then':
// Allow returning a temporary reference from an async function
// Unlike regular Client References, a Promise would never have been serialized as
// an opaque Temporary Reference, but instead would have been serialized as a
// Promise on the server and so doesn't hit this path. So we can assume this wasn't
// a Promise on the client.
return undefined;
}
throw new Error(
// eslint-disable-next-line react-internal/safe-string-coercion