Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f05deca4e | ||
|
|
75be876f2a | ||
|
|
7d696dc3b8 |
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
@@ -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 |
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const YearsAndMonthsSince = () => {
|
||||
const diff = foo();
|
||||
const months = Math.floor(diff.bar());
|
||||
return <>{months}</>;
|
||||
};
|
||||
@@ -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 | }, []);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export function Foo() {
|
||||
useEffect(() => {
|
||||
try {
|
||||
// do something
|
||||
} catch ({status}) {
|
||||
// do something
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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);
|
||||
```
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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 | };
|
||||
```
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const Foo = ({json}) => {
|
||||
try {
|
||||
const foo = JSON.parse(json)?.foo;
|
||||
return <span>{foo}</span>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Bar from './Bar';
|
||||
|
||||
export function Foo() {
|
||||
return (
|
||||
<Bar
|
||||
renderer={(...props) => {
|
||||
return <span {...props}>{displayValue}</span>;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
```
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function OwnerView({
|
||||
<span
|
||||
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
|
||||
title={displayName}>
|
||||
{'<' + displayName + '>'}
|
||||
{displayName}
|
||||
</span>
|
||||
|
||||
<ElementBadges
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<{}>);
|
||||
|
||||
11
packages/react-devtools-shared/src/hydration.js
vendored
11
packages/react-devtools-shared/src/hydration.js
vendored
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
16
packages/react-server/src/ReactFizzServer.js
vendored
16
packages/react-server/src/ReactFizzServer.js
vendored
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user