Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
758686c1dc | ||
|
|
2aa5f9d4e3 | ||
|
|
8c587a2a41 | ||
|
|
12483a119b | ||
|
|
b2c30493ce | ||
|
|
36c2bf5c3e | ||
|
|
190758e623 | ||
|
|
b1a6f03f8a | ||
|
|
142fd27bf6 | ||
|
|
7ca2d4cd2e | ||
|
|
99be14c883 | ||
|
|
5a04619f60 |
@@ -1211,6 +1211,8 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
|
||||
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
|
||||
@@ -943,7 +943,10 @@ export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Alias': {
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'MaybeAlias': {
|
||||
return `MaybeAlias ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Capture': {
|
||||
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
|
||||
@@ -90,6 +90,23 @@ export type AliasingEffect =
|
||||
* c could be mutating a.
|
||||
*/
|
||||
| {kind: 'Alias'; from: Place; into: Place}
|
||||
|
||||
/**
|
||||
* Indicates the potential for information flow from `from` to `into`. This is used for a specific
|
||||
* case: functions with unknown signatures. If the compiler sees a call such as `foo(x)`, it has to
|
||||
* consider several possibilities (which may depend on the arguments):
|
||||
* - foo(x) returns a new mutable value that does not capture any information from x.
|
||||
* - foo(x) returns a new mutable value that *does* capture information from x.
|
||||
* - foo(x) returns x itself, ie foo is the identity function
|
||||
*
|
||||
* The same is true of functions that take multiple arguments: `cond(a, b, c)` could conditionally
|
||||
* return b or c depending on the value of a.
|
||||
*
|
||||
* To represent this case, MaybeAlias represents the fact that an aliasing relationship could exist.
|
||||
* Any mutations that flow through this relationship automatically become conditional.
|
||||
*/
|
||||
| {kind: 'MaybeAlias'; from: Place; into: Place}
|
||||
|
||||
/**
|
||||
* Records direct assignment: `into = from`.
|
||||
*/
|
||||
@@ -183,7 +200,8 @@ export function hashEffect(effect: AliasingEffect): string {
|
||||
case 'ImmutableCapture':
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
case 'Capture':
|
||||
case 'MaybeAlias': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.from.identifier.id,
|
||||
|
||||
@@ -85,7 +85,8 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -691,6 +691,7 @@ function applyEffect(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MaybeAlias':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
CompilerError.invariant(
|
||||
@@ -955,7 +956,7 @@ function applyEffect(
|
||||
context,
|
||||
state,
|
||||
// OK: recording information flow
|
||||
{kind: 'Alias', from: operand, into: effect.into},
|
||||
{kind: 'MaybeAlias', from: operand, into: effect.into},
|
||||
initialized,
|
||||
effects,
|
||||
);
|
||||
@@ -1323,7 +1324,7 @@ class InferenceState {
|
||||
return 'mutate-global';
|
||||
}
|
||||
case ValueKind.MaybeFrozen: {
|
||||
return 'none';
|
||||
return 'mutate-frozen';
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(kind, `Unexpected kind ${kind}`);
|
||||
@@ -2376,6 +2377,7 @@ function computeEffectsForSignature(
|
||||
// Apply substitutions
|
||||
for (const effect of signature.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'MaybeAlias':
|
||||
case 'Assign':
|
||||
case 'ImmutableCapture':
|
||||
case 'Alias':
|
||||
|
||||
@@ -160,6 +160,8 @@ export function inferMutationAliasingRanges(
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'MaybeAlias') {
|
||||
state.maybeAlias(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
@@ -247,6 +249,7 @@ export function inferMutationAliasingRanges(
|
||||
}
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
@@ -346,7 +349,8 @@ export function inferMutationAliasingRanges(
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
case 'CreateFrom':
|
||||
case 'MaybeAlias': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
@@ -567,7 +571,12 @@ type Node = {
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
maybeAliases: Map<Identifier, number>;
|
||||
edges: Array<{
|
||||
index: number;
|
||||
node: Identifier;
|
||||
kind: 'capture' | 'alias' | 'maybeAlias';
|
||||
}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
lastMutated: number;
|
||||
@@ -585,6 +594,7 @@ class AliasingState {
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
maybeAliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
@@ -630,6 +640,18 @@ class AliasingState {
|
||||
}
|
||||
}
|
||||
|
||||
maybeAlias(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'maybeAlias'});
|
||||
if (!toNode.maybeAliases.has(from.identifier)) {
|
||||
toNode.maybeAliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
@@ -673,22 +695,24 @@ class AliasingState {
|
||||
// Null is used for simulated mutations
|
||||
end: InstructionId | null,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
startKind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const seen = new Map<Identifier, MutationKind>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
kind: MutationKind;
|
||||
}> = [{place: start, transitive, direction: 'backwards', kind: startKind}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
const {place: current, transitive, direction, kind} = queue.pop()!;
|
||||
const previousKind = seen.get(current);
|
||||
if (previousKind != null && previousKind >= kind) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
seen.set(current, kind);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
continue;
|
||||
@@ -724,13 +748,18 @@ class AliasingState {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards', kind});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive: true,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
@@ -747,7 +776,25 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
queue.push({place: alias, transitive, direction: 'backwards', kind});
|
||||
}
|
||||
/**
|
||||
* MaybeAlias indicates potential data flow from unknown function calls,
|
||||
* so we downgrade mutations through these aliases to consider them
|
||||
* conditional. This means we'll consider them for mutation *range*
|
||||
* purposes but not report validation errors for mutations, since
|
||||
* we aren't sure that the `from` value could actually be aliased.
|
||||
*/
|
||||
for (const [alias, when] of node.maybeAliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({
|
||||
place: alias,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind: MutationKind.Conditional,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -758,7 +805,12 @@ class AliasingState {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
queue.push({
|
||||
place: capture,
|
||||
transitive,
|
||||
direction: 'backwards',
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
@@ -70,13 +69,6 @@ class StableSidemap {
|
||||
isStable: false,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
|
||||
isUseRefType(lvalue.identifier)
|
||||
) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes tha
|
||||
|
||||
Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis.
|
||||
|
||||
### MaybeAlias
|
||||
|
||||
Describes potential data flow that the compiler knows may occur behind a function call, but cannot be sure about. For example, `foo(x)` _may_ be the identity function and return `x`, or `cond(a, b, c)` may conditionally return `b` or `c` depending on the value of `a`, but those functions could just as easily return new mutable values and not capture any information from their arguments. MaybeAlias represents that we have to consider the potential for data flow when deciding mutable ranges, but should be conservative about reporting errors. For example, `foo(someFrozenValue).property = true` should not error since we don't know for certain that foo returns its input.
|
||||
|
||||
### State-Changing Effects
|
||||
|
||||
The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`).
|
||||
@@ -347,6 +351,17 @@ a.b = b; // capture
|
||||
mutate(a); // can transitively mutate b
|
||||
```
|
||||
|
||||
### MaybeAlias makes mutation conditional
|
||||
|
||||
Because we don't know for certain that the aliasing occurs, we consider the mutation conditional against the source.
|
||||
|
||||
```
|
||||
MaybeAlias a <- b
|
||||
Mutate a
|
||||
=>
|
||||
MutateConditional b
|
||||
```
|
||||
|
||||
### Freeze Does Not Freeze the Value
|
||||
|
||||
Freeze does not freeze the value itself:
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
InstructionKind,
|
||||
makePropertyLiteral,
|
||||
makeType,
|
||||
PropType,
|
||||
@@ -194,12 +195,29 @@ function* generateInstructionTypes(
|
||||
break;
|
||||
}
|
||||
|
||||
// We intentionally do not infer types for context variables
|
||||
// We intentionally do not infer types for most context variables
|
||||
case 'DeclareContext':
|
||||
case 'StoreContext':
|
||||
case 'LoadContext': {
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
/**
|
||||
* The caveat is StoreContext const, where we know the value is
|
||||
* assigned once such that everywhere the value is accessed, it
|
||||
* must have the same type from the rvalue.
|
||||
*
|
||||
* A concrete example where this is useful is `const ref = useRef()`
|
||||
* where the ref is referenced before its declaration in a function
|
||||
* expression, causing it to be converted to a const context variable.
|
||||
*/
|
||||
if (value.lvalue.kind === InstructionKind.Const) {
|
||||
yield equation(
|
||||
value.lvalue.place.identifier.type,
|
||||
value.value.identifier.type,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StoreLocal': {
|
||||
if (env.config.enableUseTypeAnnotations) {
|
||||
@@ -448,7 +466,36 @@ function* generateInstructionTypes(
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'PropertyStore': {
|
||||
/**
|
||||
* Infer types based on assignments to known object properties
|
||||
* This is important for refs, where assignment to `<maybeRef>.current`
|
||||
* can help us infer that an object itself is a ref
|
||||
*/
|
||||
yield equation(
|
||||
/**
|
||||
* Our property type declarations are best-effort and we haven't tested
|
||||
* using them to drive inference of rvalues from lvalues. We want to emit
|
||||
* a Property type in order to infer refs from `.current` accesses, but
|
||||
* stay conservative by not otherwise inferring anything about rvalues.
|
||||
* So we use a dummy type here.
|
||||
*
|
||||
* TODO: consider using the rvalue type here
|
||||
*/
|
||||
makeType(),
|
||||
// unify() only handles properties in the second position
|
||||
{
|
||||
kind: 'Property',
|
||||
objectType: value.object.identifier.type,
|
||||
objectName: getName(names, value.object.identifier.id),
|
||||
propertyName: {
|
||||
kind: 'literal',
|
||||
value: value.property,
|
||||
},
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'DeclareLocal':
|
||||
case 'RegExpLiteral':
|
||||
case 'MetaProperty':
|
||||
|
||||
@@ -97,16 +97,21 @@ export function validateNoSetStateInEffects(
|
||||
errors.pushDiagnostic(
|
||||
CompilerDiagnostic.create({
|
||||
category:
|
||||
'Calling setState within an effect can trigger cascading renders',
|
||||
'Calling setState synchronously within an effect can trigger cascading renders',
|
||||
description:
|
||||
'Calling setState directly within a useEffect causes cascading renders that can hurt performance, and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +
|
||||
'In general, the body of an effect should do one or both of the following:\n' +
|
||||
'* Update external systems with the latest state from React.\n' +
|
||||
'* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\n' +
|
||||
'Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. ' +
|
||||
'(https://react.dev/learn/you-might-not-need-an-effect)',
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
suggestions: null,
|
||||
}).withDetail({
|
||||
kind: 'error',
|
||||
loc: setState.loc,
|
||||
message:
|
||||
'Avoid calling setState() in the top-level of an effect',
|
||||
'Avoid calling setState() directly within an effect',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
component Example() {
|
||||
const fooRef = makeObject_Primitives();
|
||||
fooRef.current = true;
|
||||
|
||||
return <Stringify foo={fooRef} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
|
||||
4 | component Example() {
|
||||
5 | const fooRef = makeObject_Primitives();
|
||||
> 6 | fooRef.current = true;
|
||||
| ^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
|
||||
7 |
|
||||
8 | return <Stringify foo={fooRef} />;
|
||||
9 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
|
||||
import {makeObject_Primitives} from 'shared-runtime';
|
||||
|
||||
component Example() {
|
||||
const fooRef = makeObject_Primitives();
|
||||
fooRef.current = true;
|
||||
|
||||
return <Stringify foo={fooRef} />;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const frozen = useHook();
|
||||
let x;
|
||||
if (props.cond) {
|
||||
x = frozen;
|
||||
} else {
|
||||
x = {};
|
||||
}
|
||||
x.property = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed.
|
||||
|
||||
error.invalid-mutate-phi-which-could-be-frozen.ts:11:2
|
||||
9 | x = {};
|
||||
10 | }
|
||||
> 11 | x.property = true;
|
||||
| ^ value cannot be modified
|
||||
12 | }
|
||||
13 |
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const frozen = useHook();
|
||||
let x;
|
||||
if (props.cond) {
|
||||
x = frozen;
|
||||
} else {
|
||||
x = {};
|
||||
}
|
||||
x.property = true;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
// TODO: for loops create a unique environment on each iteration, which means
|
||||
// that if the iteration variable is only updated in the updater, the variable
|
||||
// is effectively const within the body and the "update" acts more like
|
||||
// a re-initialization than a reassignment.
|
||||
// Until we model this "new environment" semantic, we allow this case to error
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Found 1 error:
|
||||
|
||||
Error: This value cannot be modified
|
||||
|
||||
Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX.
|
||||
|
||||
error.todo-for-loop-with-context-variable-iterator.ts:18:30
|
||||
16 | // a re-initialization than a reassignment.
|
||||
17 | // Until we model this "new environment" semantic, we allow this case to error
|
||||
> 18 | for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
| ^ `i` cannot be modified
|
||||
19 | items.push(
|
||||
20 | <Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
21 | );
|
||||
```
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ function Component() {
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
// TODO: for loops create a unique environment on each iteration, which means
|
||||
// that if the iteration variable is only updated in the updater, the variable
|
||||
// is effectively const within the body and the "update" acts more like
|
||||
// a re-initialization than a reassignment.
|
||||
// Until we model this "new environment" semantic, we allow this case to error
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
@@ -1,89 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
const items = [];
|
||||
// NOTE: `i` is a context variable because it's reassigned and also referenced
|
||||
// within a closure, the `onClick` handler of each item
|
||||
for (let i = MIN; i <= MAX; i += INCREMENT) {
|
||||
items.push(
|
||||
<Stringify key={i} onClick={() => data.get(i)} shouldInvokeFns={true} />
|
||||
);
|
||||
}
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = new Map([
|
||||
[0, "value0"],
|
||||
[1, "value1"],
|
||||
]);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const data = useIdentity(t0);
|
||||
let t1;
|
||||
if ($[1] !== data) {
|
||||
const items = [];
|
||||
for (let i = MIN; i <= MAX; i = i + INCREMENT, i) {
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i)}
|
||||
shouldInvokeFns={true}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
t1 = <>{items}</>;
|
||||
$[1] = data;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 3;
|
||||
const INCREMENT = 1;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
params: [],
|
||||
fn: Component,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function"},"shouldInvokeFns":true}</div>
|
||||
@@ -65,7 +65,7 @@ function _temp(s) {
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"Calling setState within an effect can trigger cascading renders","description":"Calling setState directly within a useEffect causes cascading renders that can hurt performance, and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","severity":"InvalidReact","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":13,"column":4,"index":265},"end":{"line":13,"column":5,"index":266},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"},"message":"Avoid calling setState() in the top-level of an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","severity":"InvalidReact","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":13,"column":4,"index":265},"end":{"line":13,"column":5,"index":266},"filename":"invalid-setState-in-useEffect-transitive.ts","identifierName":"g"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":16,"column":1,"index":293},"filename":"invalid-setState-in-useEffect-transitive.ts"},"fnName":"Component","memoSlots":2,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function _temp(s) {
|
||||
## Logs
|
||||
|
||||
```
|
||||
{"kind":"CompileError","detail":{"options":{"category":"Calling setState within an effect can trigger cascading renders","description":"Calling setState directly within a useEffect causes cascading renders that can hurt performance, and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect)","severity":"InvalidReact","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":7,"column":4,"index":180},"end":{"line":7,"column":12,"index":188},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"},"message":"Avoid calling setState() in the top-level of an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileError","detail":{"options":{"category":"Calling setState synchronously within an effect can trigger cascading renders","description":"Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:\n* Update external systems with the latest state from React.\n* Subscribe for updates from some external system, calling setState in a callback function when external state changes.\n\nCalling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect)","severity":"InvalidReact","suggestions":null,"details":[{"kind":"error","loc":{"start":{"line":7,"column":4,"index":180},"end":{"line":7,"column":12,"index":188},"filename":"invalid-setState-in-useEffect.ts","identifierName":"setState"},"message":"Avoid calling setState() directly within an effect"}]}},"fnLoc":null}
|
||||
{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":92},"end":{"line":10,"column":1,"index":225},"filename":"invalid-setState-in-useEffect.ts"},"fnName":"Component","memoSlots":1,"memoBlocks":1,"memoValues":1,"prunedMemoBlocks":0,"prunedMemoValues":0}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
```javascript
|
||||
// @enableCustomTypeDefinitionForReanimated
|
||||
import {useAnimatedProps} from 'react-native-reanimated';
|
||||
import {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
|
||||
function Component() {
|
||||
const radius = useSharedValue(50);
|
||||
|
||||
@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableCustomTypeDefinitionForReanimated
|
||||
import { useAnimatedProps } from "react-native-reanimated";
|
||||
import { useAnimatedProps, useSharedValue } from "react-native-reanimated";
|
||||
function Component() {
|
||||
const $ = _c(2);
|
||||
const radius = useSharedValue(50);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @enableCustomTypeDefinitionForReanimated
|
||||
import {useAnimatedProps} from 'react-native-reanimated';
|
||||
import {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
|
||||
function Component() {
|
||||
const radius = useSharedValue(50);
|
||||
|
||||
|
||||
@@ -47,28 +47,32 @@ function useCustomRef() {
|
||||
function _temp() {}
|
||||
|
||||
function Foo() {
|
||||
const $ = _c(3);
|
||||
const $ = _c(4);
|
||||
const ref = useCustomRef();
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[0] !== ref) {
|
||||
t0 = () => {
|
||||
ref.current?.click();
|
||||
};
|
||||
t1 = [];
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
$[0] = ref;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t1 = $[1];
|
||||
t0 = $[1];
|
||||
}
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [];
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
useEffect(t0, t1);
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <div>foo</div>;
|
||||
$[2] = t2;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Foo() {
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
ref.current?.click();
|
||||
}, []);
|
||||
}, [ref]);
|
||||
|
||||
return <button onClick={onClick} />;
|
||||
}
|
||||
@@ -47,24 +47,26 @@ function useCustomRef() {
|
||||
function _temp() {}
|
||||
|
||||
function Foo() {
|
||||
const $ = _c(2);
|
||||
const $ = _c(4);
|
||||
const ref = useCustomRef();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[0] !== ref) {
|
||||
t0 = () => {
|
||||
ref.current?.click();
|
||||
};
|
||||
$[0] = t0;
|
||||
$[0] = ref;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t0 = $[1];
|
||||
}
|
||||
const onClick = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[2] !== onClick) {
|
||||
t1 = <button onClick={onClick} />;
|
||||
$[1] = t1;
|
||||
$[2] = onClick;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ function Foo() {
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
ref.current?.click();
|
||||
}, []);
|
||||
}, [ref]);
|
||||
|
||||
return <button onClick={onClick} />;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Foo() {
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
customRef.current?.click();
|
||||
}, []);
|
||||
}, [customRef]);
|
||||
|
||||
return <button onClick={onClick} />;
|
||||
}
|
||||
@@ -47,24 +47,26 @@ function useCustomRef() {
|
||||
function _temp() {}
|
||||
|
||||
function Foo() {
|
||||
const $ = _c(2);
|
||||
const $ = _c(4);
|
||||
const customRef = useCustomRef();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[0] !== customRef) {
|
||||
t0 = () => {
|
||||
customRef.current?.click();
|
||||
};
|
||||
$[0] = t0;
|
||||
$[0] = customRef;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
t0 = $[1];
|
||||
}
|
||||
const onClick = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
if ($[2] !== onClick) {
|
||||
t1 = <button onClick={onClick} />;
|
||||
$[1] = t1;
|
||||
$[2] = onClick;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t1 = $[3];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ function Foo() {
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
customRef.current?.click();
|
||||
}, []);
|
||||
}, [customRef]);
|
||||
|
||||
return <button onClick={onClick} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow
|
||||
component Example() {
|
||||
const fooRef = useRef();
|
||||
|
||||
function updateStyles() {
|
||||
const foo = fooRef.current;
|
||||
// The access of `barRef` here before its declaration causes it be hoisted...
|
||||
if (barRef.current == null || foo == null) {
|
||||
return;
|
||||
}
|
||||
foo.style.height = '100px';
|
||||
}
|
||||
|
||||
// ...which previously meant that we didn't infer a type...
|
||||
const barRef = useRef(null);
|
||||
|
||||
const resizeRef = useResizeObserver(
|
||||
rect => {
|
||||
const {width} = rect;
|
||||
// ...which meant that we failed to ignore the mutation here...
|
||||
barRef.current = width;
|
||||
} // ...which caused this to fail with "can't freeze a mutable function"
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver(_ => {
|
||||
updateStyles();
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={resizeRef} />;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function Example() {
|
||||
const $ = _c(6);
|
||||
const fooRef = useRef();
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = function updateStyles() {
|
||||
const foo = fooRef.current;
|
||||
if (barRef.current == null || foo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
foo.style.height = "100px";
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const updateStyles = t0;
|
||||
|
||||
const barRef = useRef(null);
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = (rect) => {
|
||||
const { width } = rect;
|
||||
|
||||
barRef.current = width;
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const resizeRef = useResizeObserver(t1);
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const observer = new ResizeObserver((_) => {
|
||||
updateStyles();
|
||||
});
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
};
|
||||
|
||||
t3 = [];
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useLayoutEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] !== resizeRef) {
|
||||
t4 = <div ref={resizeRef} />;
|
||||
$[4] = resizeRef;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,36 @@
|
||||
// @flow
|
||||
component Example() {
|
||||
const fooRef = useRef();
|
||||
|
||||
function updateStyles() {
|
||||
const foo = fooRef.current;
|
||||
// The access of `barRef` here before its declaration causes it be hoisted...
|
||||
if (barRef.current == null || foo == null) {
|
||||
return;
|
||||
}
|
||||
foo.style.height = '100px';
|
||||
}
|
||||
|
||||
// ...which previously meant that we didn't infer a type...
|
||||
const barRef = useRef(null);
|
||||
|
||||
const resizeRef = useResizeObserver(
|
||||
rect => {
|
||||
const {width} = rect;
|
||||
// ...which meant that we failed to ignore the mutation here...
|
||||
barRef.current = width;
|
||||
} // ...which caused this to fail with "can't freeze a mutable function"
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const observer = new ResizeObserver(_ => {
|
||||
updateStyles();
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={resizeRef} />;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {identity, makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const object = props.object;
|
||||
const f = () => {
|
||||
// The argument maybe-aliases into the return
|
||||
const obj = identity(object);
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
const obj = f();
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: makeObject_Primitives(), value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, makeObject_Primitives, Stringify } from "shared-runtime";
|
||||
|
||||
function Example(props) {
|
||||
const $ = _c(7);
|
||||
const object = props.object;
|
||||
let t0;
|
||||
if ($[0] !== object || $[1] !== props.value) {
|
||||
t0 = () => {
|
||||
const obj = identity(object);
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
$[0] = object;
|
||||
$[1] = props.value;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const f = t0;
|
||||
let t1;
|
||||
if ($[3] !== f) {
|
||||
t1 = f();
|
||||
$[3] = f;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const obj_0 = t1;
|
||||
let t2;
|
||||
if ($[5] !== obj_0) {
|
||||
t2 = <Stringify obj={obj_0} />;
|
||||
$[5] = obj_0;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ object: makeObject_Primitives(), value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
import {identity, makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const object = props.object;
|
||||
const f = () => {
|
||||
// The argument maybe-aliases into the return
|
||||
const obj = identity(object);
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
const obj = f();
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: makeObject_Primitives(), value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const object = props.object;
|
||||
const f = () => {
|
||||
// The receiver maybe-aliases into the return
|
||||
const obj = object.makeObject();
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
const obj = f();
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { makeObject_Primitives, Stringify } from "shared-runtime";
|
||||
|
||||
function Example(props) {
|
||||
const $ = _c(7);
|
||||
const object = props.object;
|
||||
let t0;
|
||||
if ($[0] !== object || $[1] !== props.value) {
|
||||
t0 = () => {
|
||||
const obj = object.makeObject();
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
$[0] = object;
|
||||
$[1] = props.value;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const f = t0;
|
||||
let t1;
|
||||
if ($[3] !== f) {
|
||||
t1 = f();
|
||||
$[3] = f;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const obj_0 = t1;
|
||||
let t2;
|
||||
if ($[5] !== obj_0) {
|
||||
t2 = <Stringify obj={obj_0} />;
|
||||
$[5] = obj_0;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ object: { makeObject: makeObject_Primitives }, value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const object = props.object;
|
||||
const f = () => {
|
||||
// The receiver maybe-aliases into the return
|
||||
const obj = object.makeObject();
|
||||
obj.property = props.value;
|
||||
return obj;
|
||||
};
|
||||
const obj = f();
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const obj = props.object.makeObject();
|
||||
obj.property = props.value;
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { makeObject_Primitives, Stringify } from "shared-runtime";
|
||||
|
||||
function Example(props) {
|
||||
const $ = _c(5);
|
||||
let obj;
|
||||
if ($[0] !== props.object || $[1] !== props.value) {
|
||||
obj = props.object.makeObject();
|
||||
obj.property = props.value;
|
||||
$[0] = props.object;
|
||||
$[1] = props.value;
|
||||
$[2] = obj;
|
||||
} else {
|
||||
obj = $[2];
|
||||
}
|
||||
let t0;
|
||||
if ($[3] !== obj) {
|
||||
t0 = <Stringify obj={obj} />;
|
||||
$[3] = obj;
|
||||
$[4] = t0;
|
||||
} else {
|
||||
t0 = $[4];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{ object: { makeObject: makeObject_Primitives }, value: 42 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"obj":{"a":0,"b":"value1","c":true,"property":42}}</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import {makeObject_Primitives, Stringify} from 'shared-runtime';
|
||||
|
||||
function Example(props) {
|
||||
const obj = props.object.makeObject();
|
||||
obj.property = props.value;
|
||||
return <Stringify obj={obj} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Example,
|
||||
params: [{object: {makeObject: makeObject_Primitives}, value: 42}],
|
||||
};
|
||||
13
packages/react-client/src/ReactFlightClient.js
vendored
13
packages/react-client/src/ReactFlightClient.js
vendored
@@ -3647,7 +3647,7 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void {
|
||||
// $FlowFixMe[cannot-write]
|
||||
ioInfo.end += response._timeOrigin;
|
||||
|
||||
if (response._replayConsole) {
|
||||
if (enableComponentPerformanceTrack && response._replayConsole) {
|
||||
const env = response._rootEnvironmentName;
|
||||
const promise = ioInfo.value;
|
||||
if (promise) {
|
||||
@@ -4149,7 +4149,10 @@ function processFullStringRow(
|
||||
return;
|
||||
}
|
||||
case 78 /* "N" */: {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
// Track the time origin for future debug info. We track it relative
|
||||
// to the current environment's time space.
|
||||
const timeOrigin: number = +row;
|
||||
@@ -4169,11 +4172,7 @@ function processFullStringRow(
|
||||
// Fallthrough to share the error with Console entries.
|
||||
}
|
||||
case 74 /* "J" */: {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
if (enableProfilerTimer && enableAsyncDebugInfo) {
|
||||
resolveIOInfo(response, id, row);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export function logComponentRender(
|
||||
}
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -125,7 +125,7 @@ export function logComponentRender(
|
||||
);
|
||||
} else {
|
||||
console.timeStamp(
|
||||
entryName,
|
||||
'\u200b' + entryName,
|
||||
startTime < 0 ? 0 : startTime,
|
||||
childrenEndTime,
|
||||
trackNames[trackIdx],
|
||||
@@ -163,7 +163,7 @@ export function logComponentAborted(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
performance.measure(entryName, {
|
||||
performance.measure('\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -220,7 +220,7 @@ export function logComponentErrored(
|
||||
if (componentInfo.props != null) {
|
||||
addObjectToProperties(componentInfo.props, properties, 0, '');
|
||||
}
|
||||
performance.measure(entryName, {
|
||||
performance.measure('\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: childrenEndTime,
|
||||
detail: {
|
||||
@@ -614,7 +614,7 @@ export function logIOInfoErrored(
|
||||
getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected';
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
@@ -667,7 +667,7 @@ export function logIOInfo(
|
||||
);
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, entryName, {
|
||||
performance.measure.bind(performance, '\u200b' + entryName, {
|
||||
start: startTime < 0 ? 0 : startTime,
|
||||
end: endTime,
|
||||
detail: {
|
||||
|
||||
@@ -2898,7 +2898,7 @@ describe('ReactFlight', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableAsyncIterableChildren
|
||||
// @gate enableAsyncIterableChildren && enableComponentPerformanceTrack
|
||||
it('preserves debug info for server-to-server pass through of async iterables', async () => {
|
||||
let resolve;
|
||||
const iteratorPromise = new Promise(r => (resolve = r));
|
||||
@@ -3727,7 +3727,7 @@ describe('ReactFlight', () => {
|
||||
expect(caughtError.digest).toBe('digest("my-error")');
|
||||
});
|
||||
|
||||
// @gate __DEV__ && enableComponentPerformanceTrack
|
||||
// @gate __DEV__ && enableComponentPerformanceTrack
|
||||
it('can render deep but cut off JSX in debug info', async () => {
|
||||
function createDeepJSX(n) {
|
||||
if (n <= 0) {
|
||||
|
||||
10
packages/react-devtools-core/src/standalone.js
vendored
10
packages/react-devtools-core/src/standalone.js
vendored
@@ -26,7 +26,7 @@ import {
|
||||
import {localStorageSetItem} from 'react-devtools-shared/src/storage';
|
||||
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
|
||||
|
||||
export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error';
|
||||
export type StatusListener = (message: string, status: StatusTypes) => void;
|
||||
@@ -144,8 +144,8 @@ async function fetchFileWithCaching(url: string) {
|
||||
}
|
||||
|
||||
function canViewElementSourceFunction(
|
||||
_source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
_source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
): boolean {
|
||||
if (symbolicatedSource == null) {
|
||||
return false;
|
||||
@@ -156,8 +156,8 @@ function canViewElementSourceFunction(
|
||||
}
|
||||
|
||||
function viewElementSourceFunction(
|
||||
_source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
_source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
): void {
|
||||
if (symbolicatedSource == null) {
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,12 @@ import {
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
getAlwaysOpenInEditor,
|
||||
getOpenInEditorURL,
|
||||
normalizeUrlIfValid,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils';
|
||||
|
||||
import {
|
||||
setBrowserSelectionFromReact,
|
||||
@@ -326,7 +331,7 @@ function createSourcesEditorPanel() {
|
||||
editorPane = createdPane;
|
||||
|
||||
createdPane.setPage('panel.html');
|
||||
createdPane.setHeight('42px');
|
||||
createdPane.setHeight('75px');
|
||||
|
||||
createdPane.onShown.addListener(portal => {
|
||||
editorPortalContainer = portal.container;
|
||||
@@ -530,3 +535,45 @@ if (__IS_FIREFOX__) {
|
||||
connectExtensionPort();
|
||||
|
||||
mountReactDevToolsWhenReactHasLoaded();
|
||||
|
||||
function onThemeChanged(themeName) {
|
||||
// Rerender with the new theme
|
||||
render();
|
||||
}
|
||||
|
||||
if (chrome.devtools.panels.setThemeChangeHandler) {
|
||||
// Chrome
|
||||
chrome.devtools.panels.setThemeChangeHandler(onThemeChanged);
|
||||
} else if (chrome.devtools.panels.onThemeChanged) {
|
||||
// Firefox
|
||||
chrome.devtools.panels.onThemeChanged.addListener(onThemeChanged);
|
||||
}
|
||||
|
||||
// Firefox doesn't support resources handlers yet.
|
||||
if (chrome.devtools.panels.setOpenResourceHandler) {
|
||||
chrome.devtools.panels.setOpenResourceHandler(
|
||||
(
|
||||
resource,
|
||||
lineNumber = 1,
|
||||
// The column is a new feature so we have to specify a default if it doesn't exist
|
||||
columnNumber = 1,
|
||||
) => {
|
||||
const alwaysOpenInEditor = getAlwaysOpenInEditor();
|
||||
const editorURL = getOpenInEditorURL();
|
||||
if (alwaysOpenInEditor && editorURL) {
|
||||
const location = ['', resource.url, lineNumber, columnNumber];
|
||||
const {url, shouldDisableButton} = checkConditions(editorURL, location);
|
||||
if (!shouldDisableButton) {
|
||||
window.open(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Otherwise fallback to the built-in behavior.
|
||||
chrome.devtools.panels.openResource(
|
||||
resource.url,
|
||||
lineNumber - 1,
|
||||
columnNumber - 1,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,17 +34,26 @@ export type ReactFunctionLocation = [
|
||||
number, // enclosing line number
|
||||
number, // enclosing column number
|
||||
];
|
||||
export type ReactCallSite = [
|
||||
string, // function name
|
||||
string, // file name TODO: model nested eval locations as nested arrays
|
||||
number, // line number
|
||||
number, // column number
|
||||
number, // enclosing line number
|
||||
number, // enclosing column number
|
||||
boolean, // async resume
|
||||
];
|
||||
export type ViewElementSource = (
|
||||
source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
) => void;
|
||||
export type ViewAttributeSource = (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
) => void;
|
||||
export type CanViewElementSource = (
|
||||
source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
) => boolean;
|
||||
|
||||
export type InitializationOptions = {
|
||||
|
||||
@@ -15,8 +15,7 @@ export function test(maybeInspectedElement) {
|
||||
hasOwnProperty('canEditFunctionProps') &&
|
||||
hasOwnProperty('canEditHooks') &&
|
||||
hasOwnProperty('canToggleSuspense') &&
|
||||
hasOwnProperty('canToggleError') &&
|
||||
hasOwnProperty('canViewSource')
|
||||
hasOwnProperty('canToggleError')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4374,8 +4374,6 @@ export function attach(
|
||||
(fiber.alternate !== null &&
|
||||
forceFallbackForFibers.has(fiber.alternate))),
|
||||
|
||||
// Can view component source location.
|
||||
canViewSource,
|
||||
source,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
@@ -4416,7 +4414,6 @@ export function attach(
|
||||
function inspectVirtualInstanceRaw(
|
||||
virtualInstance: VirtualInstance,
|
||||
): InspectedElement | null {
|
||||
const canViewSource = true;
|
||||
const source = getSourceForInstance(virtualInstance);
|
||||
|
||||
const componentInfo = virtualInstance.data;
|
||||
@@ -4470,8 +4467,6 @@ export function attach(
|
||||
|
||||
canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary,
|
||||
|
||||
// Can view component source location.
|
||||
canViewSource,
|
||||
source,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
|
||||
@@ -830,8 +830,6 @@ export function attach(
|
||||
// Suspense did not exist in legacy versions
|
||||
canToggleSuspense: false,
|
||||
|
||||
// Can view component source location.
|
||||
canViewSource: type === ElementTypeClass || type === ElementTypeFunction,
|
||||
source: null,
|
||||
|
||||
// Only legacy context exists in legacy versions.
|
||||
|
||||
@@ -264,9 +264,6 @@ export type InspectedElement = {
|
||||
// Is this Suspense, and can its value be overridden now?
|
||||
canToggleSuspense: boolean,
|
||||
|
||||
// Can view component source location.
|
||||
canViewSource: boolean,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
hasLegacyContext: boolean,
|
||||
|
||||
|
||||
@@ -222,7 +222,6 @@ export function convertInspectedElementBackendToFrontend(
|
||||
canToggleError,
|
||||
isErrored,
|
||||
canToggleSuspense,
|
||||
canViewSource,
|
||||
hasLegacyContext,
|
||||
id,
|
||||
type,
|
||||
@@ -252,7 +251,6 @@ export function convertInspectedElementBackendToFrontend(
|
||||
canToggleError,
|
||||
isErrored,
|
||||
canToggleSuspense,
|
||||
canViewSource,
|
||||
hasLegacyContext,
|
||||
id,
|
||||
key,
|
||||
|
||||
@@ -37,6 +37,8 @@ export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL =
|
||||
'React::DevTools::openInEditorUrl';
|
||||
export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET =
|
||||
'React::DevTools::openInEditorUrlPreset';
|
||||
export const LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR =
|
||||
'React::DevTools::alwaysOpenInEditor';
|
||||
export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
|
||||
'React::DevTools::parseHookNames';
|
||||
export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
|
||||
|
||||
@@ -18,13 +18,14 @@ import Toggle from '../Toggle';
|
||||
import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
|
||||
import InspectedElementView from './InspectedElementView';
|
||||
import {InspectedElementContext} from './InspectedElementContext';
|
||||
import {getOpenInEditorURL} from '../../../utils';
|
||||
import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
|
||||
import {getAlwaysOpenInEditor} from '../../../utils';
|
||||
import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants';
|
||||
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
|
||||
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
|
||||
import OpenInEditorButton from './OpenInEditorButton';
|
||||
import InspectedElementViewSourceButton from './InspectedElementViewSourceButton';
|
||||
import Skeleton from './Skeleton';
|
||||
import useEditorURL from '../useEditorURL';
|
||||
|
||||
import styles from './InspectedElement.css';
|
||||
|
||||
@@ -118,18 +119,21 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
inspectedElement != null &&
|
||||
inspectedElement.canToggleSuspense;
|
||||
|
||||
const editorURL = useSyncExternalStore(
|
||||
function subscribe(callback) {
|
||||
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
const alwaysOpenInEditor = useSyncExternalStore(
|
||||
useCallback(function subscribe(callback) {
|
||||
window.addEventListener(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, callback);
|
||||
return function unsubscribe() {
|
||||
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
window.removeEventListener(
|
||||
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
},
|
||||
function getState() {
|
||||
return getOpenInEditorURL();
|
||||
},
|
||||
}, []),
|
||||
getAlwaysOpenInEditor,
|
||||
);
|
||||
|
||||
const editorURL = useEditorURL();
|
||||
|
||||
const toggleErrored = useCallback(() => {
|
||||
if (inspectedElement == null) {
|
||||
return;
|
||||
@@ -217,7 +221,8 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!editorURL &&
|
||||
{!alwaysOpenInEditor &&
|
||||
!!editorURL &&
|
||||
inspectedElement != null &&
|
||||
inspectedElement.source != null &&
|
||||
symbolicatedSourcePromise != null && (
|
||||
@@ -271,8 +276,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
|
||||
|
||||
{!hideViewSourceAction && (
|
||||
<InspectedElementViewSourceButton
|
||||
canViewSource={inspectedElement?.canViewSource}
|
||||
source={inspectedElement?.source}
|
||||
source={inspectedElement ? inspectedElement.source : null}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useCallback, useContext} from 'react';
|
||||
import {copy} from 'clipboard-js';
|
||||
import {toNormalUrl} from 'jsc-safe-url';
|
||||
|
||||
@@ -17,7 +16,7 @@ import ButtonIcon from '../ButtonIcon';
|
||||
import Skeleton from './Skeleton';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
|
||||
import ViewElementSourceContext from './ViewElementSourceContext';
|
||||
import useOpenResource from '../useOpenResource';
|
||||
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import styles from './InspectedElementSourcePanel.css';
|
||||
@@ -91,24 +90,11 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
|
||||
function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
|
||||
const symbolicatedSource = React.use(symbolicatedSourcePromise);
|
||||
|
||||
const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
|
||||
ViewElementSourceContext,
|
||||
const [linkIsEnabled, viewSource] = useOpenResource(
|
||||
source,
|
||||
symbolicatedSource,
|
||||
);
|
||||
|
||||
// In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
|
||||
// To detect this case, we defer to an injected helper function (if present).
|
||||
const linkIsEnabled =
|
||||
viewElementSourceFunction != null &&
|
||||
source != null &&
|
||||
(canViewElementSourceFunction == null ||
|
||||
canViewElementSourceFunction(source, symbolicatedSource));
|
||||
|
||||
const viewSource = useCallback(() => {
|
||||
if (viewElementSourceFunction != null && source != null) {
|
||||
viewElementSourceFunction(source, symbolicatedSource);
|
||||
}
|
||||
}, [source, symbolicatedSource]);
|
||||
|
||||
const [, sourceURL, line] =
|
||||
symbolicatedSource == null ? source : symbolicatedSource;
|
||||
|
||||
|
||||
@@ -11,79 +11,48 @@ import * as React from 'react';
|
||||
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Button from '../Button';
|
||||
import ViewElementSourceContext from './ViewElementSourceContext';
|
||||
import Skeleton from './Skeleton';
|
||||
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {
|
||||
CanViewElementSource,
|
||||
ViewElementSource,
|
||||
} from 'react-devtools-shared/src/devtools/views/DevTools';
|
||||
|
||||
const {useCallback, useContext} = React;
|
||||
import useOpenResource from '../useOpenResource';
|
||||
|
||||
type Props = {
|
||||
canViewSource: ?boolean,
|
||||
source: ?ReactFunctionLocation,
|
||||
source: null | ReactFunctionLocation,
|
||||
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
|
||||
};
|
||||
|
||||
function InspectedElementViewSourceButton({
|
||||
canViewSource,
|
||||
source,
|
||||
symbolicatedSourcePromise,
|
||||
}: Props): React.Node {
|
||||
const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
|
||||
ViewElementSourceContext,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
|
||||
<ActualSourceButton
|
||||
canViewSource={canViewSource}
|
||||
source={source}
|
||||
symbolicatedSourcePromise={symbolicatedSourcePromise}
|
||||
canViewElementSourceFunction={canViewElementSourceFunction}
|
||||
viewElementSourceFunction={viewElementSourceFunction}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
type ActualSourceButtonProps = {
|
||||
canViewSource: ?boolean,
|
||||
source: ?ReactFunctionLocation,
|
||||
source: null | ReactFunctionLocation,
|
||||
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
|
||||
canViewElementSourceFunction: CanViewElementSource | null,
|
||||
viewElementSourceFunction: ViewElementSource | null,
|
||||
};
|
||||
function ActualSourceButton({
|
||||
canViewSource,
|
||||
source,
|
||||
symbolicatedSourcePromise,
|
||||
canViewElementSourceFunction,
|
||||
viewElementSourceFunction,
|
||||
}: ActualSourceButtonProps): React.Node {
|
||||
const symbolicatedSource =
|
||||
symbolicatedSourcePromise == null
|
||||
? null
|
||||
: React.use(symbolicatedSourcePromise);
|
||||
|
||||
// In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
|
||||
// To detect this case, we defer to an injected helper function (if present).
|
||||
const buttonIsEnabled =
|
||||
!!canViewSource &&
|
||||
viewElementSourceFunction != null &&
|
||||
source != null &&
|
||||
(canViewElementSourceFunction == null ||
|
||||
canViewElementSourceFunction(source, symbolicatedSource));
|
||||
|
||||
const viewSource = useCallback(() => {
|
||||
if (viewElementSourceFunction != null && source != null) {
|
||||
viewElementSourceFunction(source, symbolicatedSource);
|
||||
}
|
||||
}, [source, symbolicatedSource]);
|
||||
|
||||
const [buttonIsEnabled, viewSource] = useOpenResource(
|
||||
source,
|
||||
symbolicatedSource,
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
disabled={!buttonIsEnabled}
|
||||
|
||||
@@ -51,22 +51,22 @@ import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContex
|
||||
import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
|
||||
import type {SourceSelection} from './Editor/EditorPane';
|
||||
|
||||
export type TabID = 'components' | 'profiler';
|
||||
|
||||
export type ViewElementSource = (
|
||||
source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
) => void;
|
||||
export type ViewAttributeSource = (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
) => void;
|
||||
export type CanViewElementSource = (
|
||||
source: ReactFunctionLocation,
|
||||
symbolicatedSource: ReactFunctionLocation | null,
|
||||
source: ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
|
||||
) => boolean;
|
||||
|
||||
export type Props = {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
.EditorPane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: block;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family-sans);
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.EditorPane, .EditorPane * {
|
||||
@@ -14,6 +11,19 @@
|
||||
-webkit-font-smoothing: var(--font-smoothing);
|
||||
}
|
||||
|
||||
.EditorToolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.EditorInfo {
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.VRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useSyncExternalStore, useState, startTransition} from 'react';
|
||||
import {useState, startTransition} from 'react';
|
||||
|
||||
import portaledContent from '../portaledContent';
|
||||
|
||||
@@ -18,10 +18,10 @@ import Button from 'react-devtools-shared/src/devtools/views/Button';
|
||||
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
|
||||
|
||||
import OpenInEditorButton from './OpenInEditorButton';
|
||||
import {getOpenInEditorURL} from '../../../utils';
|
||||
import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
|
||||
import useEditorURL from '../useEditorURL';
|
||||
|
||||
import EditorSettings from './EditorSettings';
|
||||
import CodeEditorByDefault from '../Settings/CodeEditorByDefault';
|
||||
|
||||
export type SourceSelection = {
|
||||
url: string,
|
||||
@@ -36,22 +36,38 @@ export type Props = {selectedSource: ?SourceSelection};
|
||||
|
||||
function EditorPane({selectedSource}: Props) {
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showLinkInfo, setShowLinkInfo] = useState(false);
|
||||
|
||||
const editorURL = useSyncExternalStore(
|
||||
function subscribe(callback) {
|
||||
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
return function unsubscribe() {
|
||||
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
};
|
||||
},
|
||||
function getState() {
|
||||
return getOpenInEditorURL();
|
||||
},
|
||||
);
|
||||
const editorURL = useEditorURL();
|
||||
|
||||
if (showSettings) {
|
||||
if (showLinkInfo) {
|
||||
return (
|
||||
<div className={styles.EditorPane}>
|
||||
<div className={styles.EditorToolbar}>
|
||||
<div style={{display: 'flex', flex: '1 1 auto'}}>
|
||||
To enable link handling in your browser's DevTools settings, look
|
||||
for the option Extension -> Link Handling. Select "React Developer
|
||||
Tools".
|
||||
</div>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
onClick={() =>
|
||||
startTransition(() => {
|
||||
setShowLinkInfo(false);
|
||||
setShowSettings(false);
|
||||
})
|
||||
}>
|
||||
<ButtonIcon type="close" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let editorToolbar;
|
||||
if (showSettings) {
|
||||
editorToolbar = (
|
||||
<div className={styles.EditorToolbar}>
|
||||
<EditorSettings />
|
||||
<div className={styles.VRule} />
|
||||
<Button onClick={() => startTransition(() => setShowSettings(false))}>
|
||||
@@ -59,24 +75,43 @@ function EditorPane({selectedSource}: Props) {
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
editorToolbar = (
|
||||
<div className={styles.EditorToolbar}>
|
||||
<OpenInEditorButton
|
||||
className={styles.WideButton}
|
||||
editorURL={editorURL}
|
||||
source={selectedSource}
|
||||
/>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
onClick={() => startTransition(() => setShowSettings(true))}
|
||||
// We don't use the title here because we don't have enough space to show it.
|
||||
// Once we expand this pane we can add it.
|
||||
// title="Configure code editor"
|
||||
>
|
||||
<ButtonIcon type="settings" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.EditorPane}>
|
||||
<OpenInEditorButton
|
||||
className={styles.WideButton}
|
||||
editorURL={editorURL}
|
||||
source={selectedSource}
|
||||
/>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
onClick={() => startTransition(() => setShowSettings(true))}
|
||||
// We don't use the title here because we don't have enough space to show it.
|
||||
// Once we expand this pane we can add it.
|
||||
// title="Configure code editor"
|
||||
>
|
||||
<ButtonIcon type="settings" />
|
||||
</Button>
|
||||
{editorToolbar}
|
||||
<div className={styles.EditorInfo}>
|
||||
{editorURL ? (
|
||||
<CodeEditorByDefault
|
||||
onChange={alwaysOpenInEditor => {
|
||||
if (alwaysOpenInEditor) {
|
||||
startTransition(() => setShowLinkInfo(true));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
'Configure an external editor to open local files.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
|
||||
|
||||
export function checkConditions(
|
||||
editorURL: string,
|
||||
source: ReactFunctionLocation,
|
||||
source: ReactFunctionLocation | ReactCallSite,
|
||||
): {url: URL | null, shouldDisableButton: boolean} {
|
||||
try {
|
||||
const url = new URL(editorURL);
|
||||
|
||||
const [, sourceURL, line] = source;
|
||||
const [, sourceURL, line, column] = source;
|
||||
let filePath;
|
||||
|
||||
// Check if sourceURL is a correct URL, which has a protocol specified
|
||||
@@ -47,12 +47,15 @@ export function checkConditions(
|
||||
}
|
||||
|
||||
const lineNumberAsString = String(line);
|
||||
const columnNumberAsString = String(column);
|
||||
|
||||
url.href = url.href
|
||||
.replace('{path}', filePath)
|
||||
.replace('{line}', lineNumberAsString)
|
||||
.replace('{column}', columnNumberAsString)
|
||||
.replace('%7Bpath%7D', filePath)
|
||||
.replace('%7Bline%7D', lineNumberAsString);
|
||||
.replace('%7Bline%7D', lineNumberAsString)
|
||||
.replace('%7Bcolumn%7D', columnNumberAsString);
|
||||
|
||||
return {url, shouldDisableButton: false};
|
||||
} catch (e) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {SchedulingEvent} from 'react-devtools-timeline/src/types';
|
||||
import * as React from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import ViewElementSourceContext from '../Components/ViewElementSourceContext';
|
||||
import {useContext} from 'react';
|
||||
import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext';
|
||||
import {
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
|
||||
import {copy} from 'clipboard-js';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
import useOpenResource from '../useOpenResource';
|
||||
|
||||
import styles from './SidebarEventInfo.css';
|
||||
|
||||
@@ -32,9 +32,6 @@ type SchedulingEventProps = {
|
||||
};
|
||||
|
||||
function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
|
||||
const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
|
||||
ViewElementSourceContext,
|
||||
);
|
||||
const {componentName, timestamp} = eventInfo;
|
||||
const componentStack = eventInfo.componentStack || null;
|
||||
|
||||
@@ -79,15 +76,10 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
|
||||
|
||||
// TODO: We should support symbolication here as well, but
|
||||
// symbolicating the whole stack can be expensive
|
||||
const canViewSource =
|
||||
canViewElementSourceFunction == null ||
|
||||
canViewElementSourceFunction(location, null);
|
||||
|
||||
const viewSource =
|
||||
!canViewSource || viewElementSourceFunction == null
|
||||
? () => null
|
||||
: () => viewElementSourceFunction(location, null);
|
||||
|
||||
const [canViewSource, viewSource] = useOpenResource(
|
||||
location,
|
||||
null,
|
||||
);
|
||||
return (
|
||||
<li key={index}>
|
||||
<Button
|
||||
|
||||
42
packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorByDefault.js
vendored
Normal file
42
packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorByDefault.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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 {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants';
|
||||
import {useLocalStorage} from '../hooks';
|
||||
|
||||
import styles from './SettingsShared.css';
|
||||
|
||||
export default function CodeEditorByDefault({
|
||||
onChange,
|
||||
}: {
|
||||
onChange?: boolean => void,
|
||||
}): React.Node {
|
||||
const [alwaysOpenInEditor, setAlwaysOpenInEditor] = useLocalStorage<boolean>(
|
||||
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
|
||||
false,
|
||||
);
|
||||
|
||||
return (
|
||||
<label className={styles.SettingRow}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={alwaysOpenInEditor}
|
||||
onChange={({currentTarget}) => {
|
||||
setAlwaysOpenInEditor(currentTarget.checked);
|
||||
if (onChange) {
|
||||
onChange(currentTarget.checked);
|
||||
}
|
||||
}}
|
||||
className={styles.SettingRowCheckbox}
|
||||
/>
|
||||
Open local files directly in your code editor
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -13,20 +13,21 @@ import {
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
} from '../../../constants';
|
||||
import {useLocalStorage} from '../hooks';
|
||||
import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
|
||||
import {
|
||||
getDefaultPreset,
|
||||
getDefaultOpenInEditorURL,
|
||||
} from 'react-devtools-shared/src/utils';
|
||||
|
||||
import styles from './SettingsShared.css';
|
||||
|
||||
const vscodeFilepath = 'vscode://file/{path}:{line}';
|
||||
|
||||
export default function ComponentsSettings({
|
||||
export default function CodeEditorOptions({
|
||||
environmentNames,
|
||||
}: {
|
||||
environmentNames: Promise<Array<string>>,
|
||||
}): React.Node {
|
||||
const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
|
||||
'vscode' | 'custom',
|
||||
>(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
|
||||
>(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, getDefaultPreset());
|
||||
|
||||
const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
||||
@@ -40,11 +41,6 @@ export default function ComponentsSettings({
|
||||
onChange={({currentTarget}) => {
|
||||
const selectedValue = currentTarget.value;
|
||||
setOpenInEditorURLPreset(selectedValue);
|
||||
if (selectedValue === 'vscode') {
|
||||
setOpenInEditorURL(vscodeFilepath);
|
||||
} else if (selectedValue === 'custom') {
|
||||
setOpenInEditorURL('');
|
||||
}
|
||||
}}>
|
||||
<option value="vscode">VS Code</option>
|
||||
<option value="custom">Custom</option>
|
||||
@@ -53,7 +49,7 @@ export default function ComponentsSettings({
|
||||
<input
|
||||
className={styles.Input}
|
||||
type="text"
|
||||
placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
|
||||
placeholder={getDefaultOpenInEditorURL()}
|
||||
value={openInEditorURL}
|
||||
onChange={event => {
|
||||
setOpenInEditorURL(event.target.value);
|
||||
|
||||
@@ -13,10 +13,14 @@ import {SettingsContext} from './SettingsContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {CHANGE_LOG_URL} from 'react-devtools-shared/src/devtools/constants';
|
||||
import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
|
||||
import CodeEditorOptions from './CodeEditorOptions';
|
||||
|
||||
import styles from './SettingsShared.css';
|
||||
|
||||
import CodeEditorOptions from './CodeEditorOptions';
|
||||
import CodeEditorByDefault from './CodeEditorByDefault';
|
||||
import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants';
|
||||
import {useLocalStorage} from '../hooks';
|
||||
|
||||
function getChangeLogUrl(version: ?string): string | null {
|
||||
if (!version) {
|
||||
return null;
|
||||
@@ -46,6 +50,11 @@ export default function GeneralSettings(_: {}): React.Node {
|
||||
const showBackendVersion =
|
||||
backendVersion && backendVersion !== frontendVersion;
|
||||
|
||||
const [alwaysOpenInEditor] = useLocalStorage<boolean>(
|
||||
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
|
||||
false,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.SettingList}>
|
||||
{isInternalFacebookBuild && (
|
||||
@@ -84,6 +93,29 @@ export default function GeneralSettings(_: {}): React.Node {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.SettingWrapper}>
|
||||
<CodeEditorByDefault />
|
||||
{alwaysOpenInEditor && (__IS_CHROME__ || __IS_EDGE__) ? (
|
||||
<div>
|
||||
To enable link handling in your browser's DevTools settings, look
|
||||
for the option Extension -> Link Handling. Select "React Developer
|
||||
Tools".
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={styles.SettingWrapper}>
|
||||
<div className={styles.RadioLabel}>Display density</div>
|
||||
<select
|
||||
value={displayDensity}
|
||||
onChange={({currentTarget}) =>
|
||||
setDisplayDensity(currentTarget.value)
|
||||
}>
|
||||
<option value="compact">Compact</option>
|
||||
<option value="comfortable">Comfortable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{supportsTraceUpdates && (
|
||||
<div className={styles.SettingWrapper}>
|
||||
<label className={styles.SettingRow}>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useLayoutEffect,
|
||||
useReducer,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import {
|
||||
@@ -162,14 +163,24 @@ export function useLocalStorage<T>(
|
||||
}
|
||||
}, [initialValue, key]);
|
||||
|
||||
const [storedValue, setStoredValue] = useState<any>(getValueFromLocalStorage);
|
||||
const storedValue = useSyncExternalStore(
|
||||
useCallback(
|
||||
function subscribe(callback) {
|
||||
window.addEventListener(key, callback);
|
||||
return function unsubscribe() {
|
||||
window.removeEventListener(key, callback);
|
||||
};
|
||||
},
|
||||
[key],
|
||||
),
|
||||
getValueFromLocalStorage,
|
||||
);
|
||||
|
||||
const setValue = useCallback(
|
||||
(value: $FlowFixMe) => {
|
||||
try {
|
||||
const valueToStore =
|
||||
value instanceof Function ? (value: any)(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
localStorageSetItem(key, JSON.stringify(valueToStore));
|
||||
|
||||
// Notify listeners that this setting has changed.
|
||||
@@ -197,7 +208,6 @@ export function useLocalStorage<T>(
|
||||
};
|
||||
|
||||
window.addEventListener('storage', onStorage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', onStorage);
|
||||
};
|
||||
|
||||
39
packages/react-devtools-shared/src/devtools/views/useEditorURL.js
vendored
Normal file
39
packages/react-devtools-shared/src/devtools/views/useEditorURL.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 {useCallback, useSyncExternalStore} from 'react';
|
||||
|
||||
import {getOpenInEditorURL} from '../../utils';
|
||||
import {
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
} from '../../constants';
|
||||
|
||||
const useEditorURL = (): string => {
|
||||
const editorURL = useSyncExternalStore(
|
||||
useCallback(function subscribe(callback) {
|
||||
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
window.addEventListener(
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
callback,
|
||||
);
|
||||
return function unsubscribe() {
|
||||
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
|
||||
window.removeEventListener(
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
}, []),
|
||||
getOpenInEditorURL,
|
||||
);
|
||||
return editorURL;
|
||||
};
|
||||
|
||||
export default useEditorURL;
|
||||
75
packages/react-devtools-shared/src/devtools/views/useOpenResource.js
vendored
Normal file
75
packages/react-devtools-shared/src/devtools/views/useOpenResource.js
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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 {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
|
||||
|
||||
import {useCallback, useContext, useSyncExternalStore} from 'react';
|
||||
|
||||
import ViewElementSourceContext from './Components/ViewElementSourceContext';
|
||||
|
||||
import {getAlwaysOpenInEditor} from '../../utils';
|
||||
import useEditorURL from './useEditorURL';
|
||||
import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../constants';
|
||||
|
||||
import {checkConditions} from './Editor/utils';
|
||||
|
||||
const useOpenResource = (
|
||||
source: null | ReactFunctionLocation | ReactCallSite,
|
||||
symbolicatedSource: null | ReactFunctionLocation | ReactCallSite,
|
||||
): [
|
||||
boolean, // isEnabled
|
||||
() => void, // Open Resource
|
||||
] => {
|
||||
const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
|
||||
ViewElementSourceContext,
|
||||
);
|
||||
|
||||
const editorURL = useEditorURL();
|
||||
|
||||
const alwaysOpenInEditor = useSyncExternalStore(
|
||||
useCallback(function subscribe(callback) {
|
||||
window.addEventListener(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, callback);
|
||||
return function unsubscribe() {
|
||||
window.removeEventListener(
|
||||
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
}, []),
|
||||
getAlwaysOpenInEditor,
|
||||
);
|
||||
|
||||
// First check if this link is eligible for being open directly in the configured editor.
|
||||
const openInEditor =
|
||||
alwaysOpenInEditor && source !== null
|
||||
? checkConditions(editorURL, symbolicatedSource || source)
|
||||
: null;
|
||||
// In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
|
||||
// To detect this case, we defer to an injected helper function (if present).
|
||||
const linkIsEnabled =
|
||||
(openInEditor !== null && !openInEditor.shouldDisableButton) ||
|
||||
(viewElementSourceFunction != null &&
|
||||
source != null &&
|
||||
(canViewElementSourceFunction == null ||
|
||||
canViewElementSourceFunction(source, symbolicatedSource)));
|
||||
|
||||
const viewSource = useCallback(() => {
|
||||
if (openInEditor !== null && !openInEditor.shouldDisableButton) {
|
||||
// If we have configured to always open in the code editor, we do so if we can.
|
||||
// Otherwise, we fallback to open in the local editor if possible (e.g. non-file urls).
|
||||
window.open(openInEditor.url);
|
||||
} else if (viewElementSourceFunction != null && source != null) {
|
||||
viewElementSourceFunction(source, symbolicatedSource);
|
||||
}
|
||||
}, [openInEditor, source, symbolicatedSource]);
|
||||
|
||||
return [linkIsEnabled, viewSource];
|
||||
};
|
||||
|
||||
export default useOpenResource;
|
||||
@@ -227,9 +227,6 @@ export type InspectedElement = {
|
||||
// Is this Suspense, and can its value be overridden now?
|
||||
canToggleSuspense: boolean,
|
||||
|
||||
// Can view component source location.
|
||||
canViewSource: boolean,
|
||||
|
||||
// Does the component have legacy context attached to it.
|
||||
hasLegacyContext: boolean,
|
||||
|
||||
|
||||
25
packages/react-devtools-shared/src/utils.js
vendored
25
packages/react-devtools-shared/src/utils.js
vendored
@@ -35,6 +35,8 @@ import {
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
|
||||
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
|
||||
SESSION_STORAGE_RECORD_TIMELINE_KEY,
|
||||
@@ -384,14 +386,27 @@ export function filterOutLocationComponentFilters(
|
||||
return componentFilters.filter(f => f.type !== ComponentFilterLocation);
|
||||
}
|
||||
|
||||
const vscodeFilepath = 'vscode://file/{path}:{line}:{column}';
|
||||
|
||||
export function getDefaultPreset(): 'custom' | 'vscode' {
|
||||
return typeof process.env.EDITOR_URL === 'string' ? 'custom' : 'vscode';
|
||||
}
|
||||
|
||||
export function getDefaultOpenInEditorURL(): string {
|
||||
return typeof process.env.EDITOR_URL === 'string'
|
||||
? process.env.EDITOR_URL
|
||||
: '';
|
||||
: vscodeFilepath;
|
||||
}
|
||||
|
||||
export function getOpenInEditorURL(): string {
|
||||
try {
|
||||
const rawPreset = localStorageGetItem(
|
||||
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
|
||||
);
|
||||
switch (rawPreset) {
|
||||
case '"vscode"':
|
||||
return vscodeFilepath;
|
||||
}
|
||||
const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL);
|
||||
if (raw != null) {
|
||||
return JSON.parse(raw);
|
||||
@@ -400,6 +415,14 @@ export function getOpenInEditorURL(): string {
|
||||
return getDefaultOpenInEditorURL();
|
||||
}
|
||||
|
||||
export function getAlwaysOpenInEditor(): boolean {
|
||||
try {
|
||||
const raw = localStorageGetItem(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR);
|
||||
return raw === 'true';
|
||||
} catch (error) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
type ParseElementDisplayNameFromBackendReturn = {
|
||||
formattedDisplayName: string | null,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
|
||||
@@ -272,7 +272,7 @@ export function logComponentRender(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(
|
||||
performance,
|
||||
name,
|
||||
'\u200b' + name,
|
||||
reusableComponentOptions,
|
||||
),
|
||||
);
|
||||
@@ -369,10 +369,10 @@ export function logComponentErrored(
|
||||
if (__DEV__ && debugTask) {
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, name, options),
|
||||
performance.measure.bind(performance, '\u200b' + name, options),
|
||||
);
|
||||
} else {
|
||||
performance.measure(name, options);
|
||||
performance.measure('\u200b' + name, options);
|
||||
}
|
||||
} else {
|
||||
console.timeStamp(
|
||||
@@ -436,10 +436,10 @@ function logComponentEffectErrored(
|
||||
if (debugTask) {
|
||||
debugTask.run(
|
||||
// $FlowFixMe[method-unbinding]
|
||||
performance.measure.bind(performance, name, options),
|
||||
performance.measure.bind(performance, '\u200b' + name, options),
|
||||
);
|
||||
} else {
|
||||
performance.measure(name, options);
|
||||
performance.measure('\u200b' + name, options);
|
||||
}
|
||||
} else {
|
||||
console.timeStamp(
|
||||
|
||||
@@ -863,4 +863,46 @@ describe('ReactFlightDOMNode', () => {
|
||||
expect(ownerStack).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
// @gate enableHalt
|
||||
it('can handle an empty prelude when prerendering', async () => {
|
||||
function App() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverAbortController = new AbortController();
|
||||
serverAbortController.abort();
|
||||
const errors = [];
|
||||
const {pendingResult} = await serverAct(async () => {
|
||||
// destructure trick to avoid the act scope from awaiting the returned value
|
||||
return {
|
||||
pendingResult: ReactServerDOMStaticServer.unstable_prerender(
|
||||
ReactServer.createElement(App, null),
|
||||
webpackMap,
|
||||
{
|
||||
signal: serverAbortController.signal,
|
||||
onError(error) {
|
||||
errors.push(error);
|
||||
},
|
||||
},
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
|
||||
const {prelude} = await pendingResult;
|
||||
|
||||
const reader = prelude.getReader();
|
||||
while (true) {
|
||||
const {done} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We don't really have an assertion other than to make sure
|
||||
// the stream doesn't hang.
|
||||
});
|
||||
});
|
||||
|
||||
117
packages/react-server/src/ReactFlightServer.js
vendored
117
packages/react-server/src/ReactFlightServer.js
vendored
@@ -731,7 +731,10 @@ function RequestInstance(
|
||||
}
|
||||
|
||||
let timeOrigin: number;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
// We start by serializing the time origin. Any future timestamps will be
|
||||
// emitted relatively to this origin. Instead of using performance.timeOrigin
|
||||
// as this origin, we use the timestamp at the start of the request.
|
||||
@@ -978,7 +981,10 @@ function serializeThenable(
|
||||
task.keyPath, // the server component sequence continues through Promise-as-a-child.
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
@@ -1048,7 +1054,10 @@ function serializeThenable(
|
||||
},
|
||||
reason => {
|
||||
if (newTask.status === PENDING) {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
// If this is async we need to time when this task finishes.
|
||||
newTask.timed = true;
|
||||
}
|
||||
@@ -1094,7 +1103,10 @@ function serializeReadableStream(
|
||||
task.keyPath,
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
@@ -1186,7 +1198,10 @@ function serializeAsyncIterable(
|
||||
task.keyPath,
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
@@ -1616,7 +1631,10 @@ function renderFunctionComponent<Props>(
|
||||
outlineComponentInfo(request, componentDebugInfo);
|
||||
|
||||
// Track when we started rendering this component.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
advanceTaskTime(request, task, performance.now());
|
||||
}
|
||||
|
||||
@@ -1686,12 +1704,7 @@ function renderFunctionComponent<Props>(
|
||||
throw null;
|
||||
}
|
||||
|
||||
if (
|
||||
__DEV__ ||
|
||||
(enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo)
|
||||
) {
|
||||
if (__DEV__ || (enableProfilerTimer && enableAsyncDebugInfo)) {
|
||||
// Forward any debug information for any Promises that we use():ed during the render.
|
||||
// We do this at the end so that we don't keep doing this for each retry.
|
||||
const trackedThenables = getTrackedThenablesAfterRendering();
|
||||
@@ -2016,7 +2029,10 @@ function deferTask(request: Request, task: Task): ReactJSONValue {
|
||||
task.keyPath, // unlike outlineModel this one carries along context
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
@@ -2033,7 +2049,10 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
|
||||
task.keyPath, // unlike outlineModel this one carries along context
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack ? task.time : 0,
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
__DEV__ ? task.debugStack : null,
|
||||
__DEV__ ? task.debugTask : null,
|
||||
@@ -2482,7 +2501,10 @@ function emitAsyncSequence(
|
||||
}
|
||||
|
||||
function pingTask(request: Request, task: Task): void {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
// If this was async we need to emit the time when it completes.
|
||||
task.timed = true;
|
||||
}
|
||||
@@ -2587,7 +2609,10 @@ function createTask(
|
||||
| 'debugStack'
|
||||
| 'debugTask',
|
||||
>): any);
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
task.timed = false;
|
||||
task.time = lastTimestamp;
|
||||
}
|
||||
@@ -2795,7 +2820,8 @@ function outlineModel(request: Request, value: ReactClientValue): number {
|
||||
null, // The way we use outlining is for reusing an object.
|
||||
false, // It makes no sense for that use case to be contextual.
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? performance.now() // TODO: This should really inherit the time from the task.
|
||||
: 0,
|
||||
null, // TODO: Currently we don't associate any debug information with
|
||||
@@ -3041,7 +3067,8 @@ function serializeBlob(request: Request, blob: Blob): string {
|
||||
null,
|
||||
false,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? performance.now() // TODO: This should really inherit the time from the task.
|
||||
: 0,
|
||||
null, // TODO: Currently we don't associate any debug information with
|
||||
@@ -3177,7 +3204,8 @@ function renderModel(
|
||||
task.keyPath,
|
||||
task.implicitSlot,
|
||||
request.abortableTasks,
|
||||
enableProfilerTimer && enableComponentPerformanceTrack
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
? task.time
|
||||
: 0,
|
||||
__DEV__ ? task.debugOwner : null,
|
||||
@@ -5130,11 +5158,7 @@ function forwardDebugInfoFromThenable(
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
if (enableProfilerTimer && enableAsyncDebugInfo) {
|
||||
const sequence = getAsyncSequenceFromPromise(thenable);
|
||||
if (sequence !== null) {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, owner, stack);
|
||||
@@ -5155,11 +5179,7 @@ function forwardDebugInfoFromCurrentContext(
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
if (enableProfilerTimer && enableAsyncDebugInfo) {
|
||||
const sequence = getCurrentAsyncSequence();
|
||||
if (sequence !== null) {
|
||||
emitAsyncSequence(request, task, sequence, debugInfo, null, null);
|
||||
@@ -5182,11 +5202,7 @@ function forwardDebugInfoFromAbortedTask(request: Request, task: Task): void {
|
||||
forwardDebugInfo(request, task, debugInfo);
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
enableComponentPerformanceTrack &&
|
||||
enableAsyncDebugInfo
|
||||
) {
|
||||
if (enableProfilerTimer && enableAsyncDebugInfo) {
|
||||
let thenable: null | Thenable<any> = null;
|
||||
if (typeof model.then === 'function') {
|
||||
thenable = (model: any);
|
||||
@@ -5262,7 +5278,10 @@ function advanceTaskTime(
|
||||
task: Task,
|
||||
timestamp: number,
|
||||
): void {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
if (
|
||||
!enableProfilerTimer ||
|
||||
(!enableComponentPerformanceTrack && !enableAsyncDebugInfo)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Emits a timing chunk, if the new timestamp is higher than the previous timestamp of this task.
|
||||
@@ -5278,7 +5297,10 @@ function advanceTaskTime(
|
||||
}
|
||||
|
||||
function markOperationEndTime(request: Request, task: Task, timestamp: number) {
|
||||
if (!enableProfilerTimer || !enableComponentPerformanceTrack) {
|
||||
if (
|
||||
!enableProfilerTimer ||
|
||||
(!enableComponentPerformanceTrack && !enableAsyncDebugInfo)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// This is like advanceTaskTime() but always emits a timing chunk even if it doesn't advance.
|
||||
@@ -5384,7 +5406,10 @@ function emitChunk(
|
||||
}
|
||||
|
||||
function erroredTask(request: Request, task: Task, error: mixed): void {
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
if (task.timed) {
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
}
|
||||
@@ -5467,7 +5492,10 @@ function retryTask(request: Request, task: Task): void {
|
||||
}
|
||||
}
|
||||
// We've finished rendering. Log the end time.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
if (task.timed) {
|
||||
markOperationEndTime(request, task, performance.now());
|
||||
}
|
||||
@@ -5605,7 +5633,10 @@ function finishAbortedTask(
|
||||
}
|
||||
forwardDebugInfoFromAbortedTask(request, task);
|
||||
// Track when we aborted this task as its end time.
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
if (task.timed) {
|
||||
markOperationEndTime(request, task, request.abortTime);
|
||||
}
|
||||
@@ -5762,6 +5793,7 @@ function flushCompletedChunks(request: Request): void {
|
||||
// TODO: If this destination is not currently flowing we'll not close it when it resumes flowing.
|
||||
// We should keep a separate status for this.
|
||||
if (request.destination !== null) {
|
||||
request.status = CLOSED;
|
||||
close(request.destination);
|
||||
request.destination = null;
|
||||
}
|
||||
@@ -5779,8 +5811,8 @@ function flushCompletedChunks(request: Request): void {
|
||||
);
|
||||
request.cacheController.abort(abortReason);
|
||||
}
|
||||
request.status = CLOSED;
|
||||
if (request.destination !== null) {
|
||||
request.status = CLOSED;
|
||||
close(request.destination);
|
||||
request.destination = null;
|
||||
}
|
||||
@@ -5920,7 +5952,10 @@ export function abort(request: Request, reason: mixed): void {
|
||||
}
|
||||
try {
|
||||
request.status = ABORTING;
|
||||
if (enableProfilerTimer && enableComponentPerformanceTrack) {
|
||||
if (
|
||||
enableProfilerTimer &&
|
||||
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
|
||||
) {
|
||||
request.abortTime = performance.now();
|
||||
}
|
||||
request.cacheController.abort(reason);
|
||||
|
||||
@@ -610,7 +610,7 @@ describe('ReactFlightAsyncDebugInfo', () => {
|
||||
expect(entries).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"name": "Component",
|
||||
"name": "\u200bComponent",
|
||||
},
|
||||
{
|
||||
"name": "await getData (…/pulls)",
|
||||
|
||||
@@ -247,7 +247,7 @@ export const enableProfilerCommitHooks = __PROFILE__;
|
||||
// Phase param passed to onRender callback differentiates between an "update" and a "cascading-update".
|
||||
export const enableProfilerNestedUpdatePhase = __PROFILE__;
|
||||
|
||||
export const enableAsyncDebugInfo = __EXPERIMENTAL__;
|
||||
export const enableAsyncDebugInfo = true;
|
||||
|
||||
// Track which Fiber(s) schedule render work.
|
||||
export const enableUpdaterTracking = __PROFILE__;
|
||||
|
||||
Reference in New Issue
Block a user